3. Crazy geek


The original:
https://overreacted.io/react-…


This article first send WeChat messages public number: jingchengyideng welcome attention, every day to push you fresh front-end technology articles


Many tutorials introduce React as a UI library. This makes sense because React itself is a UI library. Just like the official website says.

I’ve written before about the challenges of building user interfaces. But this article will talk about React in a different way — because it is more like a programming runtime.

This article will not teach you any tips on how to create an interface. But it will probably give you a deeper understanding of the React programming model.


⚠️ Note: If you are still learning React, please go to the official documentation

This article will be very in-depth — so it’s not for beginners. In this article, I will try to illustrate the React programming model from a best principles perspective. I won’t explain how to use it — I’ll explain how it works.

This article is for experienced programmers and anyone who has used other UI libraries but weighed the pros and cons of a project and chose React. I hope it’s helpful!

Some people have been using React for years without ever thinking about what I’m going to talk about. This is definitely a programmer’s view of React, not a designer’s. But I don’t think it hurts to relearn React from two different perspectives.

Without further ado, let’s start to understand React in depth!


The host trees

Some programs output numbers. Others output poetry. Different languages and their runtimes are often optimized for a specific set of use cases, and React is no exception.

The React program typically outputs a tree that changes over time. It could be a DOM tree, an iOS view layer, a PDF primitive, or a JSON object. But usually we want to use it to show the UI. We call it a “host tree” because it tends to be part of the host environment outside of React — like DOM or iOS. The host tree usually has its own imperative API. React is the layer on top of it.

So what’s the use of React? Very abstract, it helps you write applications that are predictable and can manipulate complex host trees to respond to external events like user interactions, network responses, timers, etc.

A professional tool is better than a generic tool when it can impose certain constraints and benefit from them. React is an example of this, and it adheres to two principles:

  • Stability. The host tree is relatively stable, and in most cases updates do not fundamentally change its overall structure. An application that rearranges all of its interactionable elements into completely different combinations every second becomes difficult to use. Where’s the button? Why is my screen dancing?
  • Versatility. The host tree can be split into UI patterns that have consistent appearance and behavior (such as buttons, lists, and avatars) rather than random shapes.

These principles apply to most UIs. But React doesn’t work when the output doesn’t have a stable “pattern.” For example, React might help you write a Twitter client, but it’s not much use for a 3D pipeline screensaver.

The host instance

The host tree consists of nodes, which we call “host instances.”

In the DOM environment, the host instance is what we normally call a DOM node — like the object you get when you call document.createElement(‘div’). In iOS, the host instance can be a value uniquely identified from JavaScript to the native view.

Host instances have their own properties (such as domNode.ClassName or View.TintColor). They may also have other host instances as children.

(This has nothing to do with React — I’m talking about the host environment.)

There is usually a native API for manipulating these host instances. For example, in the DOM environment there are APIs like AppendChild, RemoveChild, SetAttribute, and more. In React apps, you usually don’t call these APIs because that’s what React does.

The renderer

The renderer tells React how to communicate with a specific host environment and how to manage its host instance. React Dom, React Native, and even Ink can be called React renderers. You can also create your own React renderer.

The React renderer can work in one of two modes.

Most renderers are used as “mutate” mode. This pattern is exactly how the DOM works: you can create a node, set its properties, and later add or remove children. The host instance is completely mutable.

But React also works in “unchanging” mode. This pattern works for host environments that don’t provide APIs like AppendChild but clone parent trees and always replace top-level subtrees. Immutability at the level of the host tree makes multithreading much easier. React Fabric takes advantage of this pattern.

As a React user, you never have to think about these patterns. I just want to emphasize that React is not just an adapter to transition from one schema to another. It is useful for manipulating the host instance in a better way than the low-level view API paradigm.

The React elements

In a hosting environment, a host instance (such as a DOM node) is the smallest building block. In React, the smallest building block is the React element.

The React element is a normal JavaScript object. It is used to describe a host instance.

// JSX is the syntactic sugar used to describe these objects. // <button className="blue" /> { type: 'button', props: { className: 'blue' } }

The React element is lightweight because there are no host instances tied to it. Again, it’s just a description of what you want to see on the screen.

Just like the host instance, the React element can form a tree:

// JSX is the syntactic sugar used to describe these objects. // <dialog> // <button className="blue" /> // <button className="red" /> // </dialog> { type: 'dialog', props: { children: [{ type: 'button', props: { className: 'blue' } }, { type: 'button', props: { className: 'red' } }] } }

(Note: I have omitted some that are not important to the explanationattribute)

But keep in mind that the React element doesn’t exist forever. They always cycle between rebuilding and deleting.

The React element is immutable. For example, you cannot change child elements or attributes in the React element. If you want to render something different later, you need to create a new React element tree from scratch to describe it.

I like to compare the React element to every single frame shown in a movie. They capture what the UI looks like at a particular point in time. They will never change again.

The entrance

Every React renderer has an “entry”. It’s that particular API that lets us tell React to render a particular React element tree into a real host instance.

For example, the gateway to React Dom is reactdom.render:

ReactDOM.render(
  // { type: 'button', props: { className: 'blue' } }
  <button className="blue" />,
  document.getElementById('container')
);

When we call ReactDom.Render (ReactElement, DomContainer), we’re saying, “Dear React, map my ReactElement to the host tree of DomContaienR. “

React looks at ReactEmement.type (in our case, button) and tells the React DOM renderer to create the corresponding host instance and set the correct properties:

Function createHostInstance(reactElement) {let domNode = document.createElement(reactElement.type); domNode.className = reactElement.props.className; return domNode; }

In our example, React would do this:

let domNode = document.createElement('button');
domNode.className = 'blue';
domContainer.appendChild(domNode);

If the React element in reactElement. Props. Child element is contained in the children, the React will recursively apply colours to a drawing for the first time for them to create a host instances.

coordinate

What happens if we call reactdom.render () twice from the same container?

ReactDOM.render( <button className="blue" />, document.getElementById('container') ); / /... After... // Should we replace the Button host instance? // Or update the property on an existing button? ReactDOM.render( <button className="red" />, document.getElementById('container') );

Similarly, React’s job is to map the React element tree to the host tree. Determining what to do to the host instance in response to new information is sometimes called coordination.

There are two ways to solve this. A simplified version of React discards an existing tree and creates it from scratch:

let domContainer = document.getElementById('container'); // Clear the original tree domContainer.innerHTML = "; // Let domNode = document.createElement('button'); domNode.className = 'red'; domContainer.appendChild(domNode);

But in the DOM environment, this is inefficient, and you lose focus, selection, scroll, and many other states. Instead, we want React to do something like this:

let domNode = domContainer.firstChild; // Update the existing host instance domNode.className = 'red';

In other words, React needs to decide when to update an existing host instance to match a new React element and when to recreate a new host instance.

This leads to an identification problem. The React element may be different every time. When exactly should you conceptually refer to the same host instance?

In our case, it’s very simple. We previously rendered

This is very close to how React thinks and solves these kinds of problems.

If the same element type occurs twice in the same place, React reuses the existing host instance.

Here’s an example with a comment that roughly explains how React works:

// let domNode = document.createElement('button'); // domNode.className = 'blue'; // domContainer.appendChild(domNode); ReactDOM.render( <button className="blue" />, document.getElementById('container') ); // Can host instances be reused? Can!!!! (button → button) // domNode.className = 'red'; ReactDOM.render( <button className="red" />, document.getElementById('container') ); // Can host instances be reused? Can't! (button → p) // domContainer. RemoveChild (domNode); // domNode = document.createElement('p'); // domNode.textContent = 'Hello'; // domContainer.appendChild(domNode); ReactDOM.render( <p>Hello</p>, document.getElementById('container') ); // Can host instances be reused? Can!!!! (p → p) // domNode.textContent = 'Goodbye'; ReactDOM.render( <p>Goodbye</p>, document.getElementById('container') );

The same heuristic applies to subtrees. For example, when we add two

conditions

If React only reuses host instances of element types that match before and after rendering updates, how does it render when it encounters content that contains conditional statements?

Suppose we just want to show an input field first, but then render a message before it:

// First render reactdom. render(<dialog bb0 <input /> </dialog>, domContainer); // Reactdom.render (<dialog> > <p>I was just added here! </p> <input /> </dialog>, domContainer );

In this example, the host instance will be recreated. React walks through the entire tree of elements and compares it to the previous version:

  • Dialog → dialog: Can host instances be reused? Yes – because the type matches.

    • Input - > p: Can host instances be reused?No, the type has changed!You need to delete what you already haveinputAnd then create a new onepHost instance.
    • (nothing) - > input: Need to create a new oneinputHost instance.

So React will perform updates like this:

let oldInputNode = dialogNode.firstChild; dialogNode.removeChild(oldInputNode); let pNode = document.createElement('p'); pNode.textContent = 'I was just added here! '; dialogNode.appendChild(pNode); let newInputNode = document.createElement('input'); dialogNode.appendChild(newInputNode);

This is unscientific because is not actually replaced by

— it just moves the position. We don’t want to lose the selection, focus, and other states and their contents by rebuilding the DOM.

While this problem is easy to fix (I’ll get to it in a second below), it’s not a common problem in React applications. And it’s interesting to think about why that is.

In fact, you rarely call reactdom.render directly. Instead, in the React application, programs are often broken down into functions like this:

function Form({ showMessage }) { let message = null; if (showMessage) { message = <p>I was just added here! </p>; } return ( <dialog> {message} <input /> </dialog> ); }

This example does not run into the problems we have just described. Let’s use object annotations instead of JSX to better understand why. Consider the tree of child elements in dialog:

function Form({ showMessage }) { let message = null; if (showMessage) { message = { type: 'p', props: { children: 'I was just added here! '}}; } return { type: 'dialog', props: { children: [ message, { type: 'input', props: {} } ] } }; }

Whether showMessage is true or false, <input> is always in the second child’s position during rendering and does not change.

If showMessage changes from false to true, React traverses the entire tree of elements and compares it with the previous version:

  • Dialog → dialog: Can I reuse the host instance? Yes – because the type matches.

    • (null) - > p: Need to insert a new onepHost instance.
    • The input to the input: Can you reuse the host instance?Yes – because the type matches.

Then React will execute the code roughly like this:

let inputNode = dialogNode.firstChild; let pNode = document.createElement('p'); pNode.textContent = 'I was just added here! '; dialogNode.insertBefore(pNode, inputNode);

This way the state in the input box is not lost.

The list of

Comparing element types at the same place in the tree is often sufficient to determine whether to reuse or rebuild the corresponding host instance.

But this only works if the children are stationary and do not reorder. In the example above, even if message does not exist, we still know that the input box follows the message and that there are no other child elements.

When we have dynamic lists, we can’t be sure that the order is always the same.

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

If our list of items is reordered, React just sees that all the P’s and the input inside have the same type and doesn’t know how to move them. (In React’s opinion, while the items themselves have changed, the order hasn’t.)

So React will reorder the 10 items like this:

for (let i = 0; i < 10; i++) {
  let pNode = formNode.childNodes[i];
  let textNode = pNode.firstChild;
  textNode.textContent = 'You bought ' + items[i].name;
}

React only updates each element instead of reordering it. Doing so can cause performance problems and potential bugs. For example, when the order of a list of items is changed, the item that was in the first entry box will still be in the first entry box – despite the fact that it should represent other items in the list!

That’s why React asks you to specify an attribute called key every time the output contains an array of elements:

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p key={item.productId}>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

Key gives React the ability to determine whether child elements are truly the same, even if their position in the parent element is not the same before and after rendering.

When React finds

in

, it checks whether

in the previous version of . This method works even if the children in change their positions. When the keys remain the same before and after rendering, React reuses the previous host instance and then reorders its sibling elements.

Note that the key is only associated with a specific parent React element, such as

. React does not match child elements whose parents are different but whose keys are the same. (React does not have idiomatic support for moving a host instance between different parent elements without recreating the element.)

What is the best value to assign to a key? The best answer is: when can you say that an element will not change even if its order in the parent element is changed? For example, in our list of items, the ID of the item itself is the unique identifier that distinguishes it from the other items, so it is the most suitable key.

component

We already know that the function returns the act element:

function Form({ showMessage }) { let message = null; if (showMessage) { message = <p>I was just added here! </p>; } return ( <dialog> {message} <input /> </dialog> ); }

These functions are called components. They allow us to build our own toolkits, such as buttons, avatars, comment boxes, and more. Components are like bread and butter for React.

The component takes one argument, an object hash. It contains “props” (short for “properties”). In this case, showMessage is a prop. They’re like named parameters.

pure

The React component should be pure for props.

Function Button(props) {// 🔴 does not work props. IsActive = true; }

In general, mutations are not idiomatic in React. (We’ll show you how to update the UI in a more generic way in response to events later on.)

However, local mutations are absolutely allowed:

function FriendList({ friends }) {
  let items = [];
  for (let i = 0; i < friends.length; i++) {
    let friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    );
  }
  return <section>{items}</section>;
}

When we create items inside the function component, we can change it any way we want, as long as these mutations occur before we use them as the final render result. So you don’t need to rewrite your code to avoid local mutations.

Similarly, lazy initialization is allowed even if it is not completely “pure” :

Function ExpenseForm () {/ / as long as it doesn't affect other components which is allowed: SuperCalculator. InitializeIfNotReady (); // Continue rendering...... }

As long as it’s safe to call a component multiple times and it doesn’t affect the rendering of other components, React doesn’t care if your code is 100% pure like strictly functional programming. In React, idempotency is more important than purity.

That is, no side effects are allowed in the React component that the user can see directly. In other words, just calling a functional component should not make any changes on the screen.

recursive

How do we use components within components? Components belong to functions so we can call them directly:

let reactElement = Form({ showMessage: true });
ReactDOM.render(reactElement, domContainer);

However, this is not the conventional way to use components in the React runtime.

Instead, we use the component idioms in the same way that we already know about the mechanism — the React element. This means that instead of calling a component function directly, React will do it for you later:

// { type: Form, props: { showMessage: true } }
let reactElement = <Form showMessage={true} />;
ReactDOM.render(reactElement, domContainer);

Then inside React, your component will be called like this:

// React something inside let type = reacement.type; // Form let props = reactElement.props; // { showMessage: true } let result = type(props); // Whatever the Form returns

Component function names are required to be capitalized. When JSX converts and sees

instead of , it makes the object type itself an identifier instead of a string:
console.log(<form />.type); // 'form' string console.log(< form />.type); / / Form function

We don’t have a global registration mechanism – we literally represent the Form when we type

. If the Form does not exist at local scope, you will find a JavaScript error, just as you would normally if you used the wrong variable name.

So what does React do when the element type is a function? It calls your component and asks the component what element it wants to render.

This step is done recursively and is described in more detail here. In general, it will do something like this:

  • You: ReactDOM.render(<App />, domContainer)
  • React: App, what do you want to render?

    • App: I want to render include<Content><Layout>
  • React:

    , what do you want to render?

    • Layout: I want to be in<div>Render my child elements in. My child element is<Content>So I guess it should render to<div>.
  • React:

    , what are you rendering?

    • <Content>: I want to be in<article>Render some text and<Footer>
  • React:

    What are you going to render?

    • <Footer>: I want to render the one with text in it<footer>
  • React: Okay, let’s get started:
<div> <article> Some text< footer> Some more text</ article> </article> </div>

That’s why we say that coordination is recursive. When React walks through the tree of elements, you might encounter that the type of the element is a component. React will call it and then continue down the React element that is returned. Eventually we’ll run out of components and React will know how to change the host tree.

The same harmonization criteria that have been discussed before apply here. If the type at the same location changes (as determined by the index and optional key), React will delete the host instance in it and rebuild it.

Inversion of control

You might be wondering: why don’t we just call the component directly? Why write

instead of Form()?

React would do better if it “knew” your components rather than the React element tree that you generated after you recursively called them.

// 🔴 React does not know Layout and Article exist. // Because you are calling them. Reactdom.render (Layout({children: Article()}), domContainer) // ✅ knows the existence of Layout and Article. // React to call them. ReactDOM.render( <Layout><Article /></Layout>, domContainer )

This is a classic case of inversion of control. By having React call our component, we get some interesting properties:

  • Components are not just functions. React can enhance component functionality with features such as local states in the tree that are closely linked to the component itself. A good runtime provides a basic abstraction that matches the problem at hand. As we already mentioned, React is specifically designed for applications that render UI trees and respond to interactions. If you call the component directly, you’ll have to build the features yourself.
  • Component types participate in coordination.Calling your component through React lets it learn more about the structure of the element tree. For example, when you render from<Feed>Turn the page toProfilePage, React doesn’t try to reuse the host instance in it – like you do<p>replace<button>The same. All state is lost — which is usually a good thing when rendering a completely different view. You don’t want to be in<PasswordForm><MessengerChat>Retain the state of the input box between though<input>The position of the “line up” unexpectedly between them.
  • React delays coordination. If you let React control call your component, it can do a lot of interesting things. For example, it lets the browser do some work between component calls so that rerendering a large component tree does not block the main thread. It would be very difficult to orchestrate this process manually without relying on React.
  • Better debugability. If the component is a first-class citizen that is valued in the library, we can build rich developer tools for self-examination in development.

One final benefit of having React call your component functions is lazy evaluation. Let’s see what it means.

Inertia is evaluated

When we call a function in JavaScript, the arguments are usually executed before the function is called.

// (2) it will be the second calculation eat(// (1) it will first calculate prepareMeal());

This is often desired by JavaScript developers because JavaScript functions may have hidden side effects. It would be very surprising if we called a function and it didn’t execute until its result had somehow been “used.”

However, the React component is relatively pure. If we knew that its results would not appear on the screen, there would be no need to execute it.

Consider the following component with

:

function Story({ currentUser }) {
  // return {
  //   type: Page,
  //   props: {
  //     user: currentUser,
  //     children: { type: Comments, props: {} }
  //   }
  // }
  return (
    <Page user={currentUser}>
      <Comments />
    </Page>
  );
}

component can render child items passed to it in

:

function Page({ user, children }) {
  return (
    <Layout>
      {children}
    </Layout>
  );
}

(in the JSX<A><B /></A><A children={<B />} />The same.

But what if there is an early return?

function Page({ user, children }) { if (! user.isLoggedIn) { return <h1>Please log in</h1>; } return ( <Layout> {children} </Layout> ); }

If we call Commonts() as a function, they will be executed immediately whether the Page wants to render them or not:

// {
//   type: Page,
//   props: {
//     children: Comments() // Always runs!
//   }
// }
<Page>
  {Comments()}
</Page>

But if we pass a React element, we don’t need to execute Comments ourselves:

// {
//   type: Page,
//   props: {
//     children: { type: Comments }
//   }
// }
<Page>
  <Comments />
</Page>

Let React decide when and if to invoke the component. If our Page component ignores its own childrenProp and instead renders

Please login

, React does not attempt to call the Comments function. What’s the point?

This is good because it both lets us avoid unnecessary rendering and makes our code less fragile. (When the user logs out, we don’t care if Comments are discarded — because it’s never called.)

state

We talked earlier about harmonization and how the conceptual “position” of elements in the tree lets React know whether to reuse the host instance or rebuild it. The host instance can have all the relevant local states: focus, selection, input, and so on. We want to preserve these states when rendering updates to conceptually the same UI. We also want to destroy them predictably when we are conceptually rendering something completely different (for example converting from

to

).

Local states are so useful that React makes them available to your components as well. Components are still functions but React enhances them with many features that are good for building UIs. One of these features is the local state that each component is bound to in the tree.

We call these features Hooks. For example, UseState is a Hook.

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

It returns a pair of values: the current state and the function that updates that state.

The array’s destructuring syntax allows us to give custom names to state variables. For example, I’m calling them count and setCount here, but they could also be called banana and setBanana. Beneath that text, we’re going to replace the second value with setState for whatever it’s called in this particular example.

You can learn more about useState and other Hooks in the React document.

consistency

Even if we want to split the coordination process itself into non-blocking working blocks, we still need to operate on the real host instance in a synchronized loop. This way we can ensure that the user does not see a semi-updated UI, and that the browser does not needlessly recalculate the layout and style of an intermediate state that the user should not see.

This is why React divides all the work into “render” and “commit” phases. The render phase is when React calls your component and then coordinates. Intervention at this stage is safe and in the future this stage will become asynchronous. The commit phase is when React manipulates the host tree. And this phase is always synchronized.

The cache

When a parent component is ready for an update through setState, React by default coordinates the entire subtree. Because React does not know whether updates in a parent component will affect its children, React defaults to consistency. This may sound like a big performance cost but it’s actually not a problem for small and medium subtrees.

When the depth and breadth of the tree reach a certain level, you can ask React to cache the subtree and reuse the previous render results when the prop is the same after the shallow comparison:

function Row({ item }) {
  // ...
}

export default React.memo(Row);

Now, when setState is called in the parent

is the same as the result of the previous rendering, React will simply skip the reconciliation process.

if the item in

You can get a fine-grained cache at the level of a single expression through the useMemo() Hook. The cache is tightly tied to its associated components and will be destroyed along with the local state. It only keeps the result of the last calculation.

By default, React does not intentionally cache components. Many components will always receive different props during the update process, so caching them will only result in a net loss.

The original model

Ironically, React does not use a “reactive” system to support fine-grained updates. In other words, any updates at the top level will only trigger coordination rather than a partial update of the affected components.

The design is intentional. Interaction time is a key metric for Web applications, and trawling through the model to set up fine-grained listeners will only waste valuable time. In addition, interactions in many applications tend to result in either small (button hover) or large (page conversion) updates, so fine-grained subscriptions simply waste memory resources.

One of the design principles of React is that it can process raw data. If you have a set of JavaScript objects obtained from a network request, you can hand them directly to the component without preprocessing them. There are no issues about which properties can be accessed, or unexpected performance shortfalls when the structure changes. React rendering is O(view size) instead of O(model size), and you can significantly reduce the view size with windowing.

There are some applications for fine-grained subscriptions that are useful for them – for example, stock symbols. This is a rare example, because “everything needs to be updated continuously at the same time”. While an imperative approach can optimize such code, React is not suitable for this situation. Again, if you want to solve this problem, you need to implement fine-grained subscriptions on top of React yourself.

Note that even fine-grained subscription and “reactive” systems do not solve some common performance problems. For example, render a very deep tree (which happens every time a page is converted) without blocking the browser. Changing the trace doesn’t make it any faster — it just makes it slower because we perform additional subscription work. Another problem is that we need to wait for the returned data before rendering the view. In React, we solve these problems with concurrent rendering.

Batch update

Some components may want to update the state in response to the same event. The following example is hypothetical, but illustrates a common pattern:

function Parent() {
  let [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      Parent clicked {count} times
      <Child />
    </div>
  );
}

function Child() {
  let [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Child clicked {count} times
    </button>
  );
}

When the event is fired, the child component’s onClick is fired first (and its setState is also fired). The parent component then calls setState in its own onClick.

If React immediately rerenders the component in response to the setState call, we end up rerendering the child component twice:

*** React browser click event handler *** Child (onClick) -setstate-re-render Child // 😞 Unnecessary rerender Parent (onClick) -setstate -Re-render parent-Re-render Child *** End the React browser click event ***

The first rendering of the Child component is wasteful. And we won’t let React skip the second rendering of the Child because the Parent might pass different data due to its own state updates.

This is why React makes batch updates after all events within a component have been triggered:

*** Child (onClick) -setState Parent (onClick) -setState *** Processing state updates *** -Re-render Parent-Re-render Child *** End the React browser click event ***

Calling setState within a component does not immediately perform a rerender. Instead, React triggers all event handlers first and then a rerender for what’s called a batch update.

Bulk updates are useful but may surprise you if your code says:

  const [count, setCounter] = useState(0);

  function increment() {
    setCounter(count + 1);
  }

  function handleClick() {
    increment();
    increment();
    increment();
  }

If we had set the initial value of count to 0, the above code would only represent three setCount(1) calls. To solve this problem, we supply setState with an “updater” function as an argument:

  const [count, setCounter] = useState(0);

  function increment() {
    setCounter(c => c + 1);
  }

  function handleClick() {
    increment();
    increment();
    increment();
  }

React will queue up the updater functions and then execute them sequentially, and eventually count will be set to 3 as a result of a rerender.

When the state logic becomes more complex than just a few setState calls, I recommend that you use UserEduer Hook to describe your local state. It’s like the “Updater” upgrade mode where you can give each update a name:

const [counter, dispatch] = useReducer((state, action) => { if (action === 'increment') { return state + 1; } else { return state; }}, 0); function handleClick() { dispatch('increment'); dispatch('increment'); dispatch('increment'); }

The Action field can be any value, although an object is a common choice.

Call tree

The runtime of a programming language often has a call stack. When function a() calls b(), and b() calls c(), there are data structures in the JavaScript engine like [a, b, c] that “track” the current location and the code to execute next. Once the C function completes its execution, its call stack frame disappears! Because it’s not needed anymore. So let’s go back to function B. When we finish executing function a, the call stack is cleared.

Of course, React runs in JavaScript and of course follows the rules of JavaScript. But you can imagine having your own call stack inside React to remember the components you are currently rendering, such as [App, Page, Layout, Article /* where it is at the moment */].

React is different from normal programming languages because it is targeted at rendering UI trees that need to be “alive” for us to interact with. The DOM operation is not executed until reactDom.render () appears for the first time.

This may be an extension of the metaphor, but I like to think of the React component as a “call tree” rather than a “call stack.” When we call the Article component, its React “Call Tree” frame is not destroyed. We need to save the local state to map to somewhere on the host instance.

These “call tree” frames are destroyed along with their local state and host instance, but are executed only when the coordination rules deem this necessary. If you’ve ever read the React source, you know that these frames are really Fibers.)

Fibers are the places where the local state really exists. When the state is updated, React marks the underlying Fibers as needing to be coordinated, and then calls those components.

context

In React, we pass data as props to other components. Sometimes, most components need the same thing – for example, the currently selected visual theme. Passing it from layer to layer would be cumbersome.

In React, we solve this problem with Context. It acts like a component’s dynamic range, allowing you to pass data from the top level and for each child component to read the value at the bottom and re-render it when the value changes:

Const ThemeContext = Reaction. createContext('light' // Default as backup); function DarkApp() { return ( <ThemeContext.Provider value="dark"> <MyComponents /> </ThemeContext.Provider> ); } function SomeDeePlyNestedChild () {// Depends on where its child component is rendered const theme = useContext(themContext); / /... }

When SomeDeePlyNestedChild is rendering, usContext (themContext) looks for the nearest < themContext.Provider> in the tree and uses its value.

(In fact, React maintains a context stack when it is rendered.)

If no ThemeContext.Provider exists, the result of the UseContext (ThemeContext) call will be replaced by the default value passed when createContext() is called. In the example above, the value is ‘light’.

Side effects

We mentioned earlier that the React component should not have observable side effects during rendering. But sometimes side effects are necessary. We may need to manage Focus state, draw with Canvas, subscribe to data sources, etc.

In React, this can all be done by declaring effect:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

If possible, React will delay the effect until the browser redraws the screen. This is good because code like subscribing to a data source does not affect interaction time or first draw time.

(There is a little-used Hook that allows you to opt out of this behavior and do some synchronous work. Please try to avoid using it.

Effect is executed more than once. It is executed when the component is first presented to the user and every subsequent update. In effect, you can touch the current props and states, such as the count in the example above.

Effect may need to be cleaned up, such as the subscription data source example. After the subscription is cleaned up, effect can return a function:

  useEffect(() => {
    DataSource.addSubscription(handleChange);
    return () => DataSource.removeSubscription(handleChange);
  });

React will execute the returned function before the next call to the effect, before the component is destroyed, of course.

Sometimes it’s not practical to call effect again on every rendering. You can tell React to skip this call if the corresponding variable does not change:

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);

However, this tends to be too early to optimize and can cause problems if you’re not familiar with how closures work in JavaScript.

For example, the following code is buggy:

useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); } []);

It is buggy because [] stands for “this effect will not be reexecuted.” But handleChange in this effect is defined outside. HandleChange may refer to any props or states:

  function handleChange() {
    console.log(count);
  }

If we do not make this effect call again, the handleChange will always be the version that was rendered the first time, and the count will always be 0.

To solve this problem, make sure you declare a specific dependency array that contains everything that can be changed, even functions:

  useEffect(() => {
    DataSource.addSubscription(handleChange);
    return () => DataSource.removeSubscription(handleChange);
  }, [handleChange]);

Depending on your code, the handleChange will be different after each render so you may still see unnecessary re-subscriptions. UseCallback can help you solve this problem. Or, you can just resubscribe. The addEventListener API in the browser, for example, is very fast, but avoiding it in a component can cause more problems than its true value.

You can learn more about useeffects and other Hooks in the React documentation.

Custom hook

Since useState and useEffect are function calls, we can combine them into our own Hooks:

function MyResponsiveComponent() { const width = useWindowWidth(); // Hook return (<p>Window width is {width}</p>); } function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }); return width; }

Custom Hooks allow different components to share reusable state logic. Note that the state itself is not shared. Each call to Hook declares only its own independent state.

You can learn more about building your own Hooks in the React document.

Static order of use

You can think of UseState as a syntax that defines the “React state variable.” It’s not really syntax, and of course, we’re still writing applications in JavaScript. But we think of React as a runtime environment because React describes the entire UI tree in JavaScript, and its features tend to be closer to the language level.

Assuming use is a syntax, it makes sense to use it at the top of a component function:

// 😉 Note: Not true syntax component Example(props) {const [count, setCount] = use State(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

What does it mean when placed inside a conditional statement or outside of a component?

// 😉 Note: Not real grammar // Whose is it... Local state? const [count, setCount] = use State(0); Component Example() {if (condition) {// What happens if condition is false? const [count, setCount] = use State(0); } function handleClick() {// What happens when you leave the component function? // What is the difference between this and a normal variable? const [count, setCount] = use State(0); }

The React state is closely tied to the components associated with it in the tree. If use were a true syntax it would also make sense when called at the top of a component function:

// 😉 Note: Not true syntax component Example(props) {// Only valid here const [count, setCount] = use State(0); If (condition) {// This will be a syntax error const [count, setCount] = use State(0); }

This is the same reason that import declarations are only useful at the top level of a module.

Of course, use is not really syntax. It won’t do much good, and it will cause a lot of trouble.

However, React does expect all Hooks calls to occur only at the top of the component and not in conditional statements. These Hooks rules can be regulated by the Linter Plugin. There’s a lot of heated debate about this design choice, but in practice I don’t see it as confusing. I also wrote about why the commonly proposed alternatives don’t work.

The internal implementation of Hooks is actually a linked list. When you call useState, we move the pointer to the next item. When we exit the component’s Call Tree frame, the list of results is cached until the next rendering begins.

This article provides a brief overview of how Hooks work internally. Arrays are perhaps a better model for explaining how they work than linked lists:

// set code; function useState() { i++; If (hooks[I]) {return hooks[I]; } // render hooks. Push (...) the first time ; } // Ready to render I = -1; hooks = fiber.hooks || []; // Call the component YourComponent(); // Cache the state of Hooks fiber. Hooks = Hooks;

(If you’re interested, the full code is inHere,).

This is roughly how every UseState () gets the right state. As we already know, “matching” is nothing new to React — in the same way that coordination depends on whether elements match before and after rendering.

What else is missing?

We’ve touched on almost all the important aspects of the React runtime environment. If you read this article, you probably know React better than 90% of devs. Yes!

Of course, there are some things I didn’t mention — mainly because we don’t know. React currently doesn’t support multi-channel rendering very well, which requires information from the child component when the parent component renders. The error handling API also does not currently have any information on Hooks. In the future, the two problems may be solved together. Concurrency mode is not stable at the moment, and there are many interesting questions about how Suspense fits into the current version. Maybe I’ll come back to them when they’re finished, and Suspense is ready for more than lazy loading can do.

I think what makes the React API so successful is that you can easily use it and go a long way without thinking about most of the topics above. In most cases, a good default feature heuristic like coordination does the right thing for us. React is kind enough to remind you when you forget to add a property like key.

If you are a UI library nerd, I hope this article will be interesting for you and will provide insight into how React works. Maybe you think React is too complicated to understand it. Either way, I’d love to hear from you on Twitter! Thank you for reading.


The first send WeChat messages public number: Jingchengyideng

Welcome to scan the two-dimensional code to pay attention to the public number, every day to push you fresh front-end technology articles


Welcome to the other highlights of this column:

  • 12 Amazing CSS Experiment Projects
  • What are the front-end interview questions at the world’s top companies
  • CSS Flexbox Visualization Manual
  • The holidays are boring? Write a little brain game in JavaScript!
  • React from a designer’s point of view
  • How does CSS sticky positioning work
  • A step-by-step guide to implementing animations using HTML5 SVG
  • Programmer 30 years old before the monthly salary is less than 30K, which way to go
  • Is third-party CSS safe?
  • Talk about the importance of super