Reputation: 572
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
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
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.
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
Upvotes: 8
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.
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.
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