How does React.useState triggers re-render?
Asked Answered
T

4

50
import { useState } from 'react';

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

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

In the above example whenever setCount(count + 1) is invoked a re-render happens. I am curious to learn the flow.

I tried looking into the source code. I could not find any reference of useState or other hooks at github.com/facebook/react.

I installed react@next via npm i react@next and found the following at node_modules/react/cjs/react.development.js

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

On tracing back for dispatcher.useState(), I could only find the following ...

function resolveDispatcher() {
  var dispatcher = ReactCurrentOwner.currentDispatcher;
  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
  return dispatcher;
}
var ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null,
  currentDispatcher: null
};

I wonder where can I find dispatcher.useState() implementation and learn how it triggers re-render when setState setCount is invoked.

Any pointer would be helpful.

Thanks!

Tumescent answered 27/10, 2018 at 17:24 Comment(2)
I would believe it is behaviour of fiber so said Reacts core algorithm. They describe it as, React Fiber is an ongoing reimplementation of React's core algorithm. github.com/acdlite/react-fiber-architecture I would believe userState -creates Object and pointer in fiber an when ever it value changes it triggers re-render. This is just a guess but I would expect something similiar to this.Hereditary
Actually, hooks in react use the useState to invoke the state update which is similar to setState. So whenever state is changed comonent willl be re-rendered.Broadcloth
F
11

The key in understanding this is the following paragraph from the Hooks FAQ

How does React associate Hook calls with components?

React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React components (or custom Hooks — which are also only called from React components).

There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.

(This also explains the Rules of Hooks. Hooks need to be called unconditionally in the same order, otherwise the association of memory cell and hook is messed up.)

Let's walk through your counter example, and see what happens. For simplicity I will refer to the compiled development React source code and React DOM source code, both version 16.13.1.

The example starts when the component mounts and useState() (defined on line 1581) is called for the first time.

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

As you have noticed, this calls resolveDispatcher() (defined on line 1546). The dispatcher refers internally to the component that's currently being rendered. Within a component you can (if you dare to get fired), have a look at the dispatcher, e.g. via

console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)

If you apply this in case of the counter example, you will notice that the dispatcher.useState() refers to the react-dom code. When the component is first mounted, useState refers to the one defined on line 15986 which calls mountState(). Upon re-rendering, the dispatcher has changed and the function useState() on line 16077 is triggered, which calls updateState(). Both methods, mountState() on line 15352 and updateState() on line 15371, return the count, setCount pair.

Tracing ReactCurrentDispatcher gets quite messy. However, the fact of its existence is already enough to understand how the re-rendering happens. The magic happens behind the scene. As the FAQ states, React keeps track of the currently rendered component. This means, useState() knows which component it is attached to, how to find the state information and how to trigger the re-rendering.

Furgeson answered 28/5, 2020 at 13:55 Comment(1)
"useState() knows which component it is attached to, how to find the state information and how to trigger the re-rendering." - the question is, we are curious how does it know? curious about the implementation & the magic behind!Veiling
B
5

I also tried to understand the logic behind useState in a very simplified and basic manner, if we just look into its basic functionalities, excluding optimizations and async behavior, then we found that it is basically doing 4 things in common,

  1. maintaining of State, primary work to do
  2. re-rendering of the component through which it get called so that caller component can get the latest value for state
  3. as it caused the re-rendering of the caller component it means it must maintain the instance or context of that component too, which also allows us to use useState for multiple component at once.
  4. as we are free to use as many useState as we want inside our component that means it must maintain some identity for each useState inside the same component.

keeping these things in mind I come up with the below snippet

const Demo = (function React() {
  let workInProgress = false;
  let context = null;

  const internalRendering = (callingContext) => {
    context = callingContext;
    context();
  };

  const intialRender = (component) => {
    context = component;
    workInProgress = true;
    context.state = [];
    context.TotalcallerId = -1; // to store the count of total number of useState within a component
    context.count = -1; // counter to keep track of useStates within component
    internalRendering(context);
    workInProgress = false;
    context.TotalcallerId = context.count;
    context = null;
  };

  const useState = (initState) => {
    if (!context) throw new Error("Can only be called inside function");

     // resetting the count so that it can maintain the order of useState being called

    context.count =
      context.count === context.TotalcallerId ? -1 : context.count; 

    let callId = ++context.count;

    // will only initialize the value of setState on initial render
    const setState =
      !workInProgress ||
      (() => {
        const instanceCallerId = callId;
        const memoizedContext = context;
        return (updatedState) => {
          memoizedContext.state[instanceCallerId].value = updatedState;
          internalRendering(memoizedContext);
        };
      })();

    context.state[callId] = context.state[callId] || {
      value: initState,
      setValue: setState,
    };

    return [context.state[callId].value, context.state[callId].setValue];
  };

  return { useState, intialRender };
})();

const { useState, intialRender } = Demo;

const Component = () => {
  const [count, setCount] = useState(1);
  const [greeting, setGreeting] = useState("hello");

  const changeCount = () => setCount(100);
  const changeGreeting = () => setGreeting("hi");

  setTimeout(() => {
    changeCount();
    changeGreeting();
  }, 5000);

  return console.log(`count ${count} name ${greeting}`);
};

const anotherComponent = () => {
  const [count, setCount] = useState(50);
  const [value, setValue] = useState("World");

  const changeCount = () => setCount(500);
  const changeValue = () => setValue("React");

  setTimeout(() => {
    changeCount();
    changeValue();
  }, 10000);

  return console.log(`count ${count} name ${value}`);
};
intialRender(Component);
intialRender(anotherComponent);

here useState and initialRender are taken from Demo. intialRender is use to call the components initially, it will initialize the context first and then on that context set the state as an empty array (there are multiple useState on each component so we need array to maintain it) and also we need counter to make count for each useState, and TotalCounter to store total number of useState being called for each component.

Branham answered 28/11, 2020 at 21:45 Comment(0)
D
4

setState is a method on the Component/PureComponent class, so it will do whatever is implemented in the Component class (including calling the render method).

setState offloads the state update to enqueueSetState so the fact that it's bound to this is really only a consequence of using classes and extending from Component. Once, you realize that the state update isn't actually being handled by the component itself and the this is just a convenient way to access the state update functionality, then useState not being explicitly bound to your component makes much more sense.

Dysplasia answered 27/10, 2018 at 17:41 Comment(1)
Thank you. I realized that I used setState in the second last sentence of the question. It was an unintentional typo, it should have been setCount. I have updated the question accordingly. Please feel free to update your answer.Tumescent
G
2

FunctionComponent is different. In the past, they are pure, simple. But now they have their own state. It's easy to forget that react use createElement wrap all the JSX node, also includes FunctionComponent.

function FunctionComponent(){
  return <div>123</div>;
}
const a=<FunctionComponent/>
//after babel transform
function FunctionComponent() {
  return React.createElement("div", null, "123");
}

var a = React.createElement(FunctionComponent, null);

The FunctionComponent was passed to react. When setState is called, it's easy to re-render;

Gate answered 5/6, 2019 at 4:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.