📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

⚙️ Весь React в 200 строках JavaScript — и Вы поймёте его навсегда

Easy IT21:01

Transcription

[music] Good day, esteemed colleagues. What if I told you that React is just a few JavaScript functions? Take my word for it. I think a significant percentage of my viewers will doubt such a bold statement. Today, we will write our own mini-React from scratch without any additional dependencies. Right on pure JavaScript, you will understand how the Virtual DOM works, what `useState` is, and why updating the interface is simply calling a function. The main idea of React is simple. The user interface is a function of the state. If the state changes, we simply recalculate the UI. Well, let's move on to our Minireact engine implementation. The code is already written. We won't waste time typing characters, but we will spend a lot of time explaining the ideas and code in detail. Let's go to the Minireact file. This file contains all the code for our engine. Before React can show anything on the screen, it needs to describe what exactly we want to display. It's like a sketch of interfaces in memory: buttons, text, nested elements, everything that will later turn into a real DOM. The `createElement` function is the heart of React's virtual world. It creates virtual nodes from which the internal element tree is built. When you write something in JSX, React, under the hood, simply calls `createElement` with the appropriate arguments. We will do the same, but with our own hands. This mini-version of the function will teach you to see that any interface is just an object describing the structure of future HTML. The `createElement` function takes several arguments: `type` is the element type, `props`, and `children`. Most of the function serves to allow us to create our own components. This entire block is responsible for the fact that a function implementing our custom component can be passed as the first parameter. And this needs to be handled somehow. This block of code is responsible for that. If a standard HTML element is passed, then this line will be sufficient. This function returns a virtual element, which we will save in the virtual DOM and then compare with the previous version. If the data of the virtual element has changed, a re-render will occur. The next function is `createFragment`. Sometimes we need to return multiple elements from a component, but without wrapping them in a `div`. This is precisely why React invented fragments, an invisible folder where you can place multiple nodes without adding anything extra to the DOM. In our Minireact, `createFragment` performs the same role, helping to group elements while preserving the frequency of the structure. It's something like an invisible container that disappears during rendering but allows the tree to remain logically sound. Our implementation is very simple. We just add a special `fragment` type and nothing more. Empty props and `children`, which was passed to the `createFragment` function. The `render` function. And now that we have the interface description, the virtual tree, it's time to bring it to life and turn it into a real DOM. The `render` function does exactly that. It reads virtual nodes, creates corresponding DOM elements, sets attributes, adds text, and recursively brings the entire tree to life. This is the moment when the imaginary interface, i.e., our virtual DOM, becomes real, and the browser shows for the first time what we described in memory. This line is for rendering a text node. In our case, it can be a string or a number. In any case, we call `document.createTextNode` and create a simple text node. This is a kind of leaf at the end of the tree, the branches of the tree. Next, if it's, if the element type is a fragment, then we process it as follows. We create a fragment and for all children of the virtual node, we call `forEach` and create, add them as children to the fragment. Well, and return the fragment. And for a regular node, i.e., for standard nodes, we call this fragment of code. Similarly for children. `changed`. This function serves to compare the old version with the new version of the node. Before redrawing the interface, we need to understand if anything has changed at all. In React, an entire system of comparing virtual trees, Fiber, does this. In our mini-version, the whole essence boils down to one single function, `changed`. It compares old and new nodes and decides whether to update this part of the interface or leave it as is. This is a simple but incredibly important idea. Thanks to this, React works fast and doesn't redraw everything. Updating the interface is not deleting and redrawing everything from scratch. It's a careful operation, almost like surgery. Change only what has actually changed. The `updateElement` function does this comparison and update step by step. If the element is new, it needs to be inserted. If it's old, deleted. If it's changed, replaced. It is this function that turns it into a reactive system where a state change automatically leads to an interface change without manual DOM manipulation. When you see React updating a button without touching the rest, that's the work of `updateElement`. In our case, the function is quite large, but its meaning is precisely this. Two functions for updating fragments. Each of the individual code blocks ends with a `return`. That is, if this block of code has executed, then we don't go further. If there is an old element of type fragment, this code is executed. If there is no old element, but there is a new one, this code is executed. Well, and so on. You can look at the source code yourself. There are comments, and it will be quite easy to understand. Next, we move on to the first hook, `useState`. How to make the interface react to user actions? We need a way to store data, update it, and automatically re-render the component when it changes. This, naturally, applies to custom user components. In React, `State` handles this. And essentially, it's just a list of values, where each value is tied to a specific component and a sequential call number. When we call `useState`, React recreates the virtual tree, and our `updateElement` system then takes care of updating the DOM. That is, reactivity is just a connection between data and the render function. No magic, no frameworks, just JavaScript. In our implementation, there are auxiliary variables. They are defined at the very beginning. There is a special `hooks` object that stores all hooks. A list of hooks for each individual component. There is `currentComponent`. We store the current component being rendered at the moment here. And `hookIndex` is the index of the hook for each component separately. Okay, good. When `useState` is called, we extract the current component. Then we check. If such a component doesn't exist, we generate an error. After that, we extract all hooks for this component, for the current one, and the hook index. And we look, if there is no data for this hook yet, we simply write the initial value into the hooks array. We create a state change function. We increment the hook. Each component can have more than one hook. Accordingly, we increment and move to the next hook if it exists. This is why it's important not to break the order of hooks within a component, otherwise, React will not be able to determine which hook we are dealing with. Then we save the updated hooks and return an array. The first element of this array is the value. The second element is the state change function. Every time a component updates, everything inside it is recreated from scratch. Well, what if we have a heavy calculation whose result rarely changes? Then the `useMemo` hook comes into play. It remembers the result of a function and recalculates it only when the dependencies change. In our Minireact, this works surprisingly simply. We store the value and the list of dependencies. If at least one dependency changes, we recalculate. If not, we return the old value. This helps to understand. Optimization in React is not a magical cache, but just a check: is it necessary to recalculate now at all? Our implementation is very similar to `useState`. In fact, all hooks will have the same code fragments because you need to get the current component, then check, then get all hooks for the current component, get the current hook index, and then the code will depend on what the hook implements. That is, the differences come later. We extract the previous hook value. By default, we assume that the dependencies have changed. Consequently, we need to return a new value. After that, we check if the dependencies have actually changed. If this function returned nothing, then we will have `false` here. And, accordingly, if there is, if there was no previous value and there were changes, or there were changes, then we save the new value. We get the new value by calling the function passed as the first parameter. And after that, we update the hooks of the current component. We increment, as in the previous hook, and return the `value`. Now that we have state and a way to update it, we need to understand what happens with each data change. `renderApp` is responsible for this process. It calls the root component, gets the new virtual tree, and starts `updateElement` to synchronize everything with the real DOM. Every click, every call to `setState` ultimately leads here. This is the point of reactivity code, the moment when data turns into an interface. In our case, when `renderApp` is called, we reset the current hook index. `currentComponent` points to the root component. Then we call the function of our app's root component and save it in the `tree` constant. Then we call `updateElement`, passing the new tree and the old tree. The old tree is saved in the `oldTree` variable. We update or update. We call `updateElement`. We update the tree. After that, we save the new tree in the `oldTree` variable, so for the next render, it will already be the old tree. And we reset `currentComponent`, assigning it to `null`. Any program must start somehow. React is no exception in this case. In our case, the `start` function is the entry point of our mini-framework. It receives the root component and the container and launches the first render. Everything else is just state updates, redraws, the infinite life cycle of React components. Great. Well, the engine is ready. With what we've written, we can already run the application and see how it works. Let's look at the root element, how it's implemented. Here we have a call to `createElement('div')`. Then `createElement('h1')`. `Counter` is also `createElement`, but the first parameter is the `Counter` component. What does the `Counter` component do? Well, let's not look at components yet, we'll talk about them a bit later. In the previous step, we learned to describe the interface by calling functions like `createElement`. And everything seems to work, but there's one catch. When the interface becomes even slightly more complex, this style turns into hell for the eyes. Nested elements are nested function calls, parentheses within parentheses, quotes, commas. At some point, even simple markup looks like a cipher. It's hard to see the structure, hard to understand where one block ends and another begins. That's why JSX syntax appeared, which looks like HTML, but under the hood does the same thing: calls `createElement`. We won't write a full-fledged parser, but we'll create a small, understandable version that will give the same effect: the ability to write the interface not as code, but as markup. Thanks to this simple parser, we will be able to write code that looks like JSX and get the same virtual tree from it that we previously built manually through function calls. That is, we return the convenience of declarativity and see that even JSX is not magic, but just a convenient form of writing for the same objects and calls. To create a parser similar to JSX, we will use a tagged template literal. This is a single function, `html`, which will be used to parse a string containing HTML-like markup. There was already a video on the channel about string tagged literals. I'll leave the link somewhere here. First, we parse the string, add these tags for each value. After that, we add a function for parsing props, a `parse` function, which serves for recursive traversal, and at the very end, we call this function. I've added detailed comments. We won't waste time on a line-by-line breakdown. Those who are interested will figure it out. Well, and I recommend watching the video about string tagged literals beforehand. Okay. Now let's see if our application works with just regular `createElement` calls and then move on to testing our JSX-like parser. At this point, we have assembled all the engine parts. We have a virtual DOM, updates, and even Mini JSX. And now we need to break down the structure of our application, the components it consists of. The first component is `app`. This is the entry point, the heart of our Minireact. It describes what exactly should appear on the screen when the program starts. There's nothing superfluous here. A regular `div`, a heading, and a single `Counter` component. This is exactly what a real React application looks like. And it all starts with the root component, which assembles the others like a builder-constructor. Currently, everything is written manually through `createElement`, but at the same time, we can use a more advanced version, easier to understand. Let's run it and see how it works. Here's the interface. We have a counter. And at the same time, this string is generated depending on the value in the counter, even or odd. Let's go back to components. Besides standard HTML elements `div` and `h1`, we have the `Counter` component. It's located in this file. And this component consists of three more elements. One element is a standard `div`. In addition, there is a `button` that we click. And there is a `Viewer`, another component. Let's look at the `Viewer` component. It's a simple visual block. It knows how to display data and nothing more. And then we'll move on to the `Counter` component, which for the first time adds interactivity to our interface. The `Viewer` component is the first real example of how data turns into an interface. Its task is simple: to display the current counter value and report whether it is even or odd. At first glance, trivial logic, but it's here for the first time that we see how React connects calculation and display. We use the `useMemo` hook to calculate the text value, even or odd, and at the same time not recalculate it unnecessarily when the number itself doesn't change. This is an important idea. React doesn't just draw elements, it optimizes updates, calculates only what really needs to be updated. With `createFragment` and `createElement`, our component turns into a small virtual structure. Just two headings, but with a full cycle of reactive updates. And all this without a single direct DOM manipulation. Only declarative description. This is how the state should look right now. When the counter value changes, React will recalculate `useMemo`, update the virtual tree, and the `Viewer` will instantly display the new state on the screen. This is true reactivity in action. Data changed, the interface adjusted itself. Now let's go back to `Counter`. Everything we've done so far is preparation. We've learned to create virtual elements, render them to the DOM, and even optimize calculations with `useMemo`. But the interface remains static until the user can interact with it. And now comes the time for pure reactivity. The `Counter` component is the very moment when our Minireact comes to life and starts reacting to user actions. Here we use the `useState` hook for the first time. It stores the current counter value and provides the `setCount` function, which triggers a re-render of the component with each state change. Well, now let's replace the implementation using `createElement` and use our JSX-like parser. We comment this out. We uncomment this. And similarly in all other components. We comment out this code, and uncomment this code. And in the root element, it's all similar. Uncomment. Comment. Save. All code is saved. And let's see. Everything works. Everything works great. The button triggers `onclick`. The value increases. React recreates the virtual tree, and the `Viewer` component receives the updated `count` and instantly displays the new value. Everything happens automatically, without direct DOM updates, without `querySelector`, without `innerHTML`. This is precisely the principle on which all of React is built. The interface is a function of the state. Data changed, the picture changed. In our case, it's a simple button and a number. But behind this button is the same architecture that works in all modern React applications. One click and a small library, written by us from scratch, shows the full power of an idea that has changed the front-end world. That's all from me. If the video was helpful, please like it. To not miss new videos, subscribe to the channel and hit the bell. Subscribe to the Telegram channel. Leave questions, remarks, suggestions, thanks, and complaints in the comments. I read them, and try to respond to the correspondence received. Thank you very much for watching, and bye-bye.