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,
\react\packages\react-reconciler\src\ReactFiberHooks.new.js
( line 2538 )
\react\packages\react-reconciler\src\ReactFiberHooks.old.js