How does React render based on changes?

Are all props checked during each render in React?

Fang Jin
JavaScript in Plain English
5 min readAug 21, 2021

--

React Profiler Figure

Let’s start from some common sense. We learned that all props are checked before each fiber is determined to render.

Suffice to say a fiber is a component or a node of a tree. In this article we use them interchangeably.

Is this right? Putting that aside, it really doesn’t tell us much, ex. where it starts and where it ends. Also it doesn’t tell us the difference between props and states either.

Let’s use one example to illustrate this matter. In above figure, a state has been changed in one of the component, the first colored block. What it defines this component can look like in the following.

const Title = () => {
const [count, dispatch] = useState(0)
const onClick = () => { dispatch(1) }
return <Child onClick={onClick} count={count />
}

It’s a quite simple component, which pass a state to its child. At one point, we clicked it therefore incrementing the state to a new value. Let’s call this fiber a source fiber where an action starts from.

Before reaching the source fiber

Let’s put down some pseudo code to see how this works. To make our life easy, let’s split the fibers into two camps, one is for fibers before reaching the source fiber, and one is for fibers under the source fiber.

When React receives an update request, it goes through the tree from the very top. Before it reaches the source fiber, every fiber along the way is bailed out by cloning the fiber from the previous scene, as outlined in the following pseudo code.

function beginWork(fiber) {
if (!anyWorkOnFiber) {
return bailoutOnAlreadyFinishedWork(fiber)
}
}

The bailout can be indicated by the dark gray color in the our Profiler Figure. These fibers stands for the fiber that has no work to do but contains some work in the children. React marks them so that it knows how to reach the source fiber. A bailout skips the render by reusing the previous reconciled children.

The same bailout applies to the light gray dotted color in the Profiler Figure. Those fibers stands for the fiber that is not in the ancestor branches of the the source fiber. So they don’t have any work nor any work in any of its children.

Under the source fiber

Ok, now we arrives to the source fiber. Because this fiber has a work to do, it needs a new render right away.

function beginWork(fiber) {
let didReceiveUpdate
const children = renderFiber(fiber) if (!didReceiveUpdate) {
return bailoutOnAlreadyFinishedWork(fiber)
}
reconcileChildren(fiber, children)
return fiber.child
}

If it’s a functional component, it invokes the function and returns the children elements that are reconciled into fibers. After all children fibers are ready, it returns the first child to continue the render work. This is basically what the beginWork does.

There’s a flag didReceiveUpdate created before the render, which is initially false, and during the render any hook can change it to true if a state change is detected. After the render, if this flag is still false, then we know there’s nothing changed inside this fiber. Thus it can be bailed out after the render. With or without the bailout, the render needs to be performed as indicated by the color blocks in the Profiler Figure.

Render starts with the source fiber

So we have two version of beginWork for fibers, before and under the source fiber. How does React switch in between? Of course React actually have a lots of other pathways, but this article tries to over-simplify it so we can at least understand the main pathway.

function beginWork(fiber) {
let didReceiveUpdate
if (oldProps !== newProps) {
didReceiveUpdate = true
beginWorkBeforeSourceFiber()
} else {
beginWorkUnderSourceFiber()
}
...
}

At the beginning of the beginWork , it checked the props of this fiber between the previous and current. If it changes, it actually believes the fiber is under the source fiber? Wow!? Notice the strict equal operator !== . It’s not checking against each prop inside, it’s checking the entire props object.

What this line translates into is if a parent is rendered, a child gets a new props, thus it needs to be rendered as well.

When a child is reconciled into a fiber, it creates a fiber with a newProps . As long as a parent renders, it enters the regime of new props and new render of all children and grand children. What could break it out of this regime is a child fiber with didReceiveUpdate to be false after the render.

Props is a passive thing

Props is really a passive thing, just as parameters of a function. It’s always there along with the component. If it were not a render, it can’t get a new delivery of props for its children, assuming at least one prop is derived from (or wired with) the state change.

If a component isn’t rendered, the props is never updated to feed into the children, thus props itself in theory can’t make a change as a cause. And what’s more interesting is that the whole props (not each individual prop inside the props) is used to check if a render should be performed.

So states and props are a bit chicken and egg thing. Let’s put it this way,

React.render(<App />, ...)

Maybe only when we invoke the render at the first time, props gets a bit active, but in preceding code, we tend to put it {} for the props. Therefore most of time the props isn’t the force at all to drive new renders.

So why sometimes we say we can make a skip of render from unchanged props? Maybe what it means by “skip” isn’t an active tense either, it just means props can be filtered off some render, such as in React.memo case.

Conclusion

Upon a state change, props are used to send info deeper with new renders. However whether a new render is performed for a fiber is determined by this state change, instead of any individual prop change.

More content at plainenglish.io

--

--

#OpenToWork Front-end Engineer, book author of “Designing React Hooks the Right Way” sold at Amazon.