Shiva
Shiva

Reputation: 572

Why React goes Infinite when I set state in function body?

If we set the state with the same value component won't re-render, but it's not applicable when I set the state in the function body.

For example, if I set the same state on the button click and the button clicked, the component does not re-rendering on the button click

function Test1() {
  const [name, setName] = useState("Shiva");
  const onButtonClick = () => {
    console.log("Clicked");
    setName("Shiva");
  };
  console.log("Redering");
  return (
    <div>
      <span>My name is {name}</span>
      <button onClick={onButtonClick}>Click Me</button>
    </div>
  );
}

But, when I set the same state before the return statement React goes infinite renderings

function Test2() {
  const [name, setName] = useState("Shiva");
  // ... come stuff
  setName("Shiva");
  console.log("Rendering");
  return (
    <div>
      <span>My name is {name}</span>
    </div>
  );
}

What actually happening internally?

Upvotes: 7

Views: 1813

Answers (2)

Dilshan
Dilshan

Reputation: 3001

React uses different approaches to schedule updates based on the place where you called your setState.

For example, your setState inside the event handler will use

export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
)

While your setState in component top level use

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
)

When you call setState, React will internally call a function,

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
)

If you check the declaration of this function, you will find a top level conditional check,

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {}

Your setState inside the event handler will use the else block while the setState in render phase ( top level function body ) will use if block.


Additional

so how does React decided whether its in render phase or not ? If you check the code of the isRenderPhaseUpdate you can see,

function isRenderPhaseUpdate(fiber: Fiber) {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}

Now you might heard about virtual DOM, actually its a linked list. Each object of the linked list is known as fiber nodes. these fiber nodes are nothing more than plain javascript objects. Each of these fiber nodes has a field called, alternate.

There can be 2 separate fiber trees ( virtual doms ). One of this fiber tree is know as current tree which is the one committed to the DOM. The other fiber tree is known as the work-in-tree. This is the one react builds newly when states updates happens yet not committed to the DOM.

So for a given Component there can be maximum two fiber nodes ( one from current tree & another one from work-in-tree ). These two fiber nodes are connected using alternate field.

currentlyRenderingFiber is a global variable which keep track the currently rendering fiber node by React.

Now you should able to understand the body of above isRenderPhaseUpdate function.

Additional Explanation Ends


CASE 01 - Event Handler

When you trigger the setState from event handler react will use else block of the above function.

If you check the body of the else block you will find out following code snippet,

const currentState: S = (queue.lastRenderedState: any);
  const eagerState = lastRenderedReducer(currentState, action);
  update.hasEagerState = true;
  update.eagerState = eagerState;
  if (is(eagerState, currentState)) { // <-- checks if the previous value is equal to current one.
     enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
     return; // <-- return early without call `enqueueConcurrentHookUpdate`
  }
  // never reaches here if the `eagerState` & `currentState` are same
  const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    const eventTime = requestEventTime();
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    entangleTransitionUpdate(root, queue, lane);
  }

As you can see when is(eagerState, currentState) true ( Which is true in your case as both eagerState & currentState holds the value "Shiva" ) React will exit from the function early without calling enqueueConcurrentHookUpdate. This is why React won't re render for the same value again.

CASE 02 - Top level of the component

When you call setState from the top level of your component, it will run when React is traversing the component tree and calling your components ( due the execution of component body by React )

Notice that React is the one who call your components. You are just defining them. React call your components and get the output when traversing aka when rendering & building the new fiber tree ( aka work-in-progress tree )

Now if you check the body of

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
) {
  // This is a render phase update. Stash it in a lazily-created map of
  // queue -> linked list of updates. After this render pass, we'll restart
  // and apply the stashed updates on top of the work-in-progress hook.
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

Notice the code comment of the above function. It creates a circular linked list of schedule updates causes from the setState in your component top level body.

According to the code comment, these stashed updates in the circular linked list will apply to the hook in your latest fiber node

Now notice the line in function enqueueRenderPhaseUpdate,

didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;

didScheduleRenderPhaseUpdateDuringThisPass is a global variable. This variable is use for a while loop,

do {
    didScheduleRenderPhaseUpdateDuringThisPass = false;

    if (numberOfReRenders >= RE_RENDER_LIMIT) {
      throw new Error(
        'Too many re-renders. React limits the number of renders to prevent ' +
          'an infinite loop.',
      );
    }
    numberOfReRenders += 1;

    // some other code

    children = Component(props, secondArg);
  } while (didScheduleRenderPhaseUpdateDuringThisPass);

Each time your component body get executed ( Component(props, secondArg); in while loop), you are triggering enqueueRenderPhaseUpdate due to the setState in your component body which sets didScheduleRenderPhaseUpdateDuringThisPass to true which triggers the while loop again which calls the Component again.

Once the loop executed 25 times, React will throw an error.

You can find these function in,

  1. \react\packages\react-reconciler\src\ReactFiberHooks.new.js ( line 2538 )
  2. \react\packages\react-reconciler\src\ReactFiberHooks.old.js

Upvotes: 8

Wazeed
Wazeed

Reputation: 1280

React uses virtual DOM to update the changes into view. Virtual DOM has the rendered state when the component is rendered into view, that is after the execution of return statement in a functional component.

Component Test1:

function Test1() {
    const [name, setName] = useState("Shiva");
    const onButtonClick = () => {
        // State updates on button click happens after component gets rendered in view
        setName("Shiva");
    };
    return (
        <div>
            <span>My name is {name}</span>
            <button onClick={onButtonClick}>Click Me</button>
        </div>
    );
}

In the above example, by the time return statement is executed, functional component has been rendered in view, thus React has updated the rendered state in it's virtual DOM for tracking during upcoming state updates. Button click event cannot occur before the component gets rendered in view, thus React is aware of rendered state, and can track changes in state to process re-rendering of component.

Component Test2:

function Test2() {
    const [name, setName] = useState("Shiva");
    // State updates before component gets rendered
    setName("Shiva");
    return (
        <div>
            <span>My name is {name}</span>
        </div>
    );
}

In the above example, state is updated before the component gets rendered into the view, thus React has not yet updated it's virtual DOM with rendered state, thus there is no rendered state to track the changes. This triggers infinite re-rendering.

Upvotes: 3

Related Questions