My recent foray into functional components has made me realize that there's a lot of confusion out there about the React rendering cycle. I'm not pointing a general finger at anyone else. I'm raising my hand and acknowledging my own misconceptions. I've been doing React development now for years, but I'm still finding places where my knowledge has been... lacking.
Naming Things Is Hard
React devs talk a lot about rendering and the rendering cycle and, if you're looking at a class component, the render()
function. The problem with these terms is that they all imply an action. Specifically, they imply that something will, well... you know... be rendered. But that's not necessarily the case at all. And misunderstanding the distinctions can be detrimental to our work.
This might be one area where the naming convention embedded in class components is, if not harmful, at least, obtuse. I say this because every single class component in React must have a render()
function defined. If there is no render()
function, the class won't compile as a React component.
Maybe that doesn't strike you as a problem, but think for a moment about how we typically name our functions. And think about what is typically implied by those names. To illustrate this, take a look at these actual function names that are drawn from one of my React projects:
const deleteUser = (userId = '') => {
// function logic here
};
const getRows = () => {
// function logic here
};
const sortUsers = (column = '', direction = '') => {
// function logic here
};
You don't need to understand anything about my app to know what these functions do. The function names clearly tell you what happens when you call them.
But there's another truth that we can imply when we see functions like these. The understanding is typically that this functionality will do what the name implies it will do every single time we call that function, and only when we call that function.
In other words, we don't need to wonder "How many times will a user be deleted?" The answer is, "As many times as the deleteUser()
function is called."
We don't need to worry about whether we are needlessly sorting-and-resorting the users. All we need to do is find anyplace in the app where sortUsers()
is being called. Because the users will be sorted whenever sortUsers()
is called, and only when sortUsers()
is called.
Now let's look at something that we see in every single class component:
export default class Yo extends React.Component {
render = () => {
return <div>Yo!</div>;
}
}
As simple as this may look, it kinda breaks our universal, fundamental understanding of exactly how functions work. Don't believe me? Well, consider these points:
Calling
render()
doesn't necessarily return anything. Inside the guts of React, I'm sure it's reasonable to state that thereturn
statement is executed every single timerender()
is called. But from the perspective of someone who doesn't live inside the React engine, this function usually won't return anything at all. In fact, since the component is stateless and the content is static, thereturn
statement really only returns anything once during its entire lifecycle, even though it may be called repeatedly.Which leads to my second point: Exactly how often will
render()
be called, anyway? Who the hell knows??? In a React application, it can be virtually impossible to know exactly when thisrender()
will be called and how often it will be called. That's because it's tied to the component lifecycle. In a React application, you never callrender()
directly. And yet,render()
gets called repeatedly, for every component, sometimes in use-cases that are hard to fully understand.Although this is somewhat semantic, "render" doesn't really describe what the
render()
function is actually doing. And I believe this accounts for at least some of the confusion. In my book, "render", in a web-based application, means something like, "I'm painting something on the screen." But there are many times that callingrender()
can result in no updates whatsoever being painted to the screen. So, from that perspective, it would probably have been clearer if the requiredrender()
function were, in fact, called something like,checkForRenderingUpdates()
, orrenderIfContentHasChanged()
. Because that's much more akin to what it's actually doing.
Greater Clarity(???) With Functions
Does this get any "better" or "cleaner" if we switch to functional components? Umm... maybe?? Consider the functional equivalent:
export default function Yo() {
return <div>Yo!</div>;
}
On one hand, we've removed the ambiguity of that render()
function because there is no render()
function. On some level, that's "good".
But I've noticed that this doesn't do much to clarify developers' understanding of how React is checking for updates. In fact, it has the potential to further obfuscate the process because there simply is no built-in indication, inside the component definition, that spells out just how-or-when this component is being re-rendered.
This can be further muddied because functional components come with none of the traditional "lifecycle methods" that we had at our disposal in class components. You can say what you want about lifecycle methods - and sometimes they can be an absolute pain to deal with. But the only thing worse than managing component lifecycle with the lifecycle methods of class components, is trying to manage lifecycle processes in functional components - which have no lifecycle methods. And at least, when you had those lifecycle methods at your disposal, they served as a tangible marker of the component's native lifecycle.
This is where I sometimes find functional components to be more confusing, and more obtuse, than class components. I've already talked to a good number of functional-programming fanboys who stridently believe that: If a functional component is called, then it is also rendered. But this simply isn't true.
It is true that, every time you call a functional component, the rendering algorithm is invoked. But that's a far cry from saying that the component is rerendered.
Static Components
Let's look at where the rendering conundrum causes a lot of confusion:
export default function App() {
const [counter, setCounter] = useState(0);
return (
<div>
<button onClick={() => setCounter(counter + 1)}>Increment ({counter})</button>
<Child/>
</div>
);
}
function Child() {
console.log('Child has been called');
return (
<div>
I am a static child.
<Grandchild/>
</div>
);
}
function Grandchild() {
console.log('Grandchild has been called');
return (
<div>I am a static grandchild.</div>
);
}
We have three layers in our app:
<App>
→ <Child>
→ <Grandchild>
<App>
is a stateful component. It holds and updates the counter
value. <Child>
and <Grandchild>
are both pure components. In fact, they're both static components. They accept no input, and they always return the same output. Although they're both descendants of <App>
, they have no dependencies upon <App>
, or <App>
's counter
variable - or upon anything else for that matter.
If you plopped <Child>
or <Grandchild>
into the middle of any other app, at any particular location, they'd do the exact same thing - every time.
So here's where it seems to me like there's still a lot of confusion out there. What happens when you click the "Increment" button?? Well, it goes like this:
- The
counter
state variable inside<App>
gets updated. - Because there has been a change to
<App>
's state,<App>
rerenders. - When
<App>
rerenders,<Child>
is called. -
<Child>
, in turn, calls<Grandchild>
.
But here's where things get sticky. The rerendering of <App>
will result in <Child>
being called. But does that mean that <Child>
was rerendered??? And will calling <Child>
, in turn, lead to <Grandchild>
being rerendered???
The answer, in both cases, is: No. At least, not in the way that you might be thinking.
(BTW, I put the console.log()
s in this example because this is exactly what I've seen other people do when they're trying to "track" when a given component is rendered. They throw these in, then they click the "Increment" button, and then they see that the console.log()
s are triggered, and they say, "See. The entire app is being rerendered every time you click the 'Increment' button." But the console.log()
s only confirm that the component is being called - not that it's being rendered.)
In this demo app, people often say that, "The entire app is being rerendered every time you click the Increment button." But at the risk of sounding like a "rules lawyer", I would reply with, "What exactly do you mean by 'rerendered'??"
Reconciling, Not Rerendering
According to the React documentation on Reconciliation, this is what's basically happening when a render()
is invoked:
When you use React, at a single point in time you can think of the
render()
function as creating a tree of React elements. On the next state or props update, thatrender()
function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.
(You can read the full documentation here: https://reactjs.org/docs/reconciliation.html)
Of course, the explanation above implies that there are differences in the before-and-after trees of React elements. If there are no differences, the diffing algorithm basically says, "do nothing".
For this reason, I almost wish that React's render()
function was instead renamed to reconcile()
. I believe that most devs think of "rendering" as being an active process of drawing/painting/displaying elements on a screen. But that's not what the render()
method does. React's rendering cycle is more like this:
const render = (previousTree, currentTree) => {
const diff = reconcile(previousTree, currentTree);
if (!diff)
return;
applyDOMUpdates(diff);
}
This is why it can be a misnomer to imply that a static component is ever truly "rerendered". The render process may be called on the static component, but that doesn't mean that the component will truly be "rerendered". Instead, what will happen is that the React engine will compare the previous tree with the current tree, it will see that there are no differences, and it will bail out of the render process.
DOM Manipulation Is Expensive, Diffing Is Not
You may see this as an inconsequential distinction. After all, whether we call it "rendering" or "reconciling", there is still some sort of comparison/computation being run every single time that we invoke the render cycle on a component. So does it really matter if the reconciliation process short circuits before any real DOM manipulation can be applied??
Yes. It matters. A lot.
We don't chase down unnecessary rerenders because our computers/browsers are so desperately constrained that they can't handle a few more CPU cycles of in-memory comparisons. We chase down unnecessary rerenders because the process of DOM manipulation is, even to this day, relatively bulky and inefficient. Browsers have come lightyears from where they were just a decade ago. But you can still drive an app to its knees by needlessly repainting UI elements in rapid succession.
Can you undermine an app's performance merely by doing in-memory comparisons of virtual DOM trees? I suppose it's technically possible. But it's extremely unlikely. Another way to think of my pseudo-code above is like this:
const render = (previousTree, currentTree) => {
const diff = quickComparison(previousTree, currentTree);
if (!diff)
return;
laboriousUpdate(diff);
}
It's almost always an unnecessary micro-optimization to be focused on the quickComparison()
. It's much more meaningful to worry about the laboriousUpdate()
.
But don't take my word for it. This is directly from the React docs, on the same page that explains the Reconciliation process (emphasis: mine):
It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same. Just to be clear, rerender in this context means calling
render
for all components, it doesn’t mean React will unmount and remount them. It will only apply the differences following the rules stated in the previous sections.
Conclusions
Obviously, I'm not trying to say that you shouldn't care about unnecessary rerenders. On some level, chasing them is part of the core definition of what it means to be a "React dev". But calling your components is not the same as rendering your components.
You should be wary of unnecessary rerenders. But you should be careful about the term "rerender". If your component is being called, but there are no updates made to the DOM, it's not really a "rerender". And it probably has no negative consequences on performances.
Top comments (10)
This is interesting. I disagree that the naming of the render function is inaccurate but I can see how someone fairly new to React would find it confusing.
I think it's important to think in terms of the virtual DOM, because it then makes more sense to think "the render method causes this component to be rendered to the virtual DOM", and subsequently React performs the diff you talked about.
You are correctly addressing the issue. But I am reading your x article and as at beginning I thought you are just pragmatist, now I think you have real problem with functions as components.
As I have nothing to classes, you can use them still and there are cases where hooks can be more confusing, whereas saying that functions as components are for FP fanboys is going to far.
BTW FP fanboys do really more badass things than writing functions without even having any impact on how these functions are finally composed in the runtime.
React team nicely have spotted the issue with lifecycle, lifecycle is not any new idea, every UI framework has some kind of lifecycle. Problem was that lifecycle was not clearly indicating when - what should be done. Hooks address this issue by targeting what dev want to do, instead when he wants to do it. So instead of when I should call effect, I just useEffect and React should do it's job about when it will be executed.
And yes not everything is great in hooks, as some things looks more implicit and tricky
Going back to the main subject - you are saying that naming of render function is wrong. And I agree it creates a confusion in terms of what renders means. Developers are rightly confusing this name with real render, and I dont blame them for that as I was confusing it also. So we are on the same page here, but in contrary to your opinion I see function as component as the solution for that. We don't see this confusing name, we just return the nodes structure. When and if this will have impact on DOM is in concern of React. And this is exactly how it should be. We define what we use and we return what we want to see, other things are covered by abstraction.
Changing name render into reconcile would be even more confusing as reconsilation is implementation detail, diffing is implementation detail. We just return what we want to render in the page, so that why they have chosen this name, as this is what you want to render, but it doesn't mean it will be.
But as I said before I agree with you that confusion between real DOM updated and calling function which returns react nodes structure is confusing. So thank you for the article.
Well, I used "fanboys" once in this article - and... that's the particular word you choose to focus on?? Umm... OK. Seems like you get a little sensitive over that word. It's nothing that bothers me.
As for your assessment of
useEffect()
, the idea that "I just useEffect and React should do it's job about when it will be executed" is... interesting. If you think thatuseEffect()
replaces all of the lifecycle methods in class components, then you apparently weren't using all of the lifecycle methods in class components.But I'm glad that
useEffect()
is so useful for you.I appreciate the feedback!
My point was - in almost every article you make you cannot handle yourself to make a point against functions in React. This article is good, but the part related to functional component has nothing to the point you are making, it's like additional throwing into the article some words against that. No sense in my opinion, you could be saying just that the confusion about the wording of rendering has not disappeared with functions. It's true.
Also in your response you needed to say that probably I didn't use lifecycle if I think hooks can replace it. It's interesting point that React team thinks you can make software by hooks only. How then hooks cannot replace lifecycle?
I am pragmatist myself. I don't like to fall into some technology as the best. But I also don't like constant pointing in something even though it has nothing to the problem.
I see you prefer classes, ok. But there are least the same amount of points against classes in React as for hooks. I prefer hooks that's it. Does it makes me a hook fanboy? If so you are class components fanboy, but I believe you don't feel as one.
It returns a virtual DOM. That's its whole contract. It doesn't return a virtual DOM only within the React framework, if you call it you would get a virtual DOM.
You can measure it if you really want to know. It may be useful for optimization sometimes. It is a form of lazy evaluation, so it is more difficult to reason it out, but not impossible.
You're going directly against this quote:
You don't see people using
allocateAndCopyConcatenate(String s1, String s2)
in Java, although that's what the+
operator does. (FYI: In Java, don't use+
to build newString
s in a loop, use aStringBuilder
.)Remembering exactly what optimizations React uses doesn't help your own optimization. Profile, profile, profile.
This is a pretty good stab at a larger class of problems imo. How to write great, self documenting libraries - and beyond that how to maintain the library once it gets a strong following.
I mostly agree that renaming
render
toreconcile
(or perhapsbuildTree
ormkVnode
...) would level up the clarity once a dev starts writing app code that needs to update itself conditionally and handle state.Now how would you address
useEffect
and its dependency array?I would rename it to
theAutoMagicalFunctionThatWasSupposedToReplaceAllTheLifecycleMethods_ButFailed()
Ouch
I like this line of posts that dig deep into concepts and semantics. I agree on many of the points.
My mental model for the render function is that it renders the component to the virtual Dom every time it is called. The reconciliation between the current and previous virtual Doms happen at the end of the cycle and may or may not result in updates to the real Dom
There is also the usual tension between imperative and declarative here. 'render' sounds imperative but it returns jsx which is declarative
You bring up a good point here. As you know from my prior posts, I genuinely like the declarative syntax. But I feel strongly that there's a logical place for imperative commands - and it can get confusing when you try to shuffle the two together.
Maciej Sikora commented here that he thinks the naming of the
render()
function makes sense. I don't personally agree. But it hadn't occurred to me that, maybe the reason I feel differently, is because I still think, often, in imperative paradigms.To me, every function should always be named after an action. And "rendering" is an action. But when you call
render()
and no changes are made to the DOM, IMHO, the function didn't do what it claimed to do.To be clear, this is semantic. And I obviously don't expect anyone on the React team to change the naming conventions. In this particular post, I only write about it because, IMHO, that ambiguity leads to outright misunderstanding on the part of a lotta devs.
As I showed in my "Static Components" demo, there are many React devs who would swear that example leads to "unnecessary rerenders". But... it doesn't.