Conditional rendering on React helps you build your apps avoiding unnecessary renders depending on some validations, and it can be used on tooltips, modals, drawer menus, etcetera. But, if we do it wrong, we can end up losing performance instead of improving our app.
It's pretty common to see something like this:
import React, {useState} from 'react';
export const MyComponent = ({}) => {
const [show, setShow] = useState(false);
return (
<p>This is my main component</p>
<MyChildComponent show={show} />
)
}
export const MyChildComponent = ({show}) => {
return show ? <p>This is my child component</p> : null;
}
That is a mistake that can potentially decrease a lot the performance of your application. Why? Because this is not conditional rendering, what we are doing in this example is returning a NULL component or, in other words, a component that renders NULL.
But you guys may think "Yeah, but...It's null, so it doesn't do anything". Au Contraire my friend, and the reason relies on its name NULL COMPONENT, and what does a component have? Right, a lifecycle. So, when we return a Null component we still have a full lifecycle that will trigger depending on what we do on their parent component.
- The true Conditional Rendering:
To avoid these problems the correct way to do is to do the conditionals on the parent component to avoid even call that child component. We're gonna be using the same example:
import React, {useState} from 'react';
export const MyComponent = ({}) => {
const [show, setShow] = useState(false);
return (
<p>This is my main component</p>
show ?? <MyChildComponent />
)
}
export const MyChildComponent = () => {
return <p>This is my child component</p>;
}
Moving the show validation to the parent component instead of the child will make the rendering to be truly conditional. The only lifecycle that will trigger in this example will be only the MyComponent
lifecycle because the MyChildComponent
isn't even being called.
- Why if we need the validation inside the component?
That can happen if we are working on legacy code and we need to fix something without changing every single one of the files where the component is being called. Then, we need to check if the validation will not change a lot in a short amount of time.
If that prop will not change a lot, we can use the memo()
function provided by React to memoize that component and avoid unnecessary re-renders on that component and improve the performance of the app without a huge refactor. But, if this prop changes a lot, then we need to change the validation as we learn before, otherwise, the performance may drop.
If you're building something like a wrapper component that will have a conditional render inside of it but will always be rendered, for example, a Tooltip component wrapper another tip can be to manage the show as a state INSIDE the tooltip component and wrap it with the memo()
function to avoid unnecessary re-renderings and prop passing to make the component reusable, autonomous and performant.
Do you have a different opinion? Do you think just like me? Do you like to add something to the post? Do it in the comments below!
I do this completely non-profit, but if you want to help me you can go here and buy me a coffee ;)
Top comments (30)
The article desperately needs a test to prove the point being made. How much is "a lot of performance"? Half a percent? Fifty percent? Is it only measurable if you render thousands of widgets or just a few?
Ok, wrote a quick test app myself: when rendering 125.000 widgets the difference is
450ms for the "outside check" option and ~2000ms when doing the check inside the component, which is quite significant.For 1000 widgets, it's ~60ms vs ~70ms and is within the error margin. Maybe just don't render 100k widgets at once :)
github.com/sergeyv/inside-outside
UPDATE: Note to self: never do performance testing on a development build :) On a production build the difference is much smaller: for 125K widgets it's 350ms "outside" versus 450ms "inside". I even went ahead and scaled it to 1.000.000 widgets, the results are ~3s vs ~5s.
I have a feeling that, in a real application, there's a very limited number of scenarios where the two approaches could show any measurable difference.
Does it affect the lighthouse score in any measurable way?
Does your app render tens or hundreds of thousands of widgets conditionally at the same time? If it does then yes, it will affect the score. The common wisdom is to avoid rendering that many widgets at once though.
I quickly tested with 1.000.000 widgets and got 78 "inside" vs 95 "outside". With 125k widgets, however, both variants got 99.
Stoked that you tried testing it out. It sounds like that test confirms its a bit of a premature optimization. Great tool in your toolbet if you ever need to render a millionish components at a time though.
Well yeah, but that's for a really simple example app, now imagine a middle-high class app with a lot of states, fetching to the server, displaying other things...
For a "real app" the difference will likely to be much less, exactly for the reason that it does many other expensive things. The test app does almost nothing but creating hundreds of thousands of widgets, so the difference is exaggerated.
A real-life example: Imagine you have cheap nails for 1 cent each and more expensive at 10c each. A ton of cheap nails would cost, say, $10K and a ton of the expensive ones will be $100K, which is a huge difference.
But if you use the nails to build some nice furniture - you only need a few dollars worth of nails in either case and the cost of the nails in the final product's price will be minuscule in either case and maybe some other considerations may become more important.
Do you happen to have benchmarks that show how much this really affects performance?
Hmmm, don't actually have a documented benchmark. But I've been seeing this a lot through multiple projects I worked on, specially on forms.
But I think is a great idea to add benchmarks, thanks! I will do it for sure!
Just wondering. I’ve never returned null in my react apps. I always just do {myBoolean && MyComponent()}. I can’t insert carot symbols but you get the point.
Exactly! This is a correct conditional render, because
MyComponent
does not render unlessmyBoolean
is trueThat's why I said that may potencially affect performance of the apps. I mean, if you have something really small or something that won't be changing a lot in the short term, you won't notice it, but still is something to work on because it's not a good practice tho...
While I agree in principle with your point - not rendering a component is preferable to rendering a null component - the reasoning is not to do with performance. React components are super cheap to instantiate, like super super cheap.
The reason you should avoid doing this is because you're not in control of the rendering behaviour of your child component, it means your pages not lay out in the way you expect it to, and it is harder to add more conditionals at the top level.
On the other hand however, there are some very valid reasons to render null. If the logic required to determine the show boolean is really tightly coupled to the rest of the child component's behaviour, it seems silly to have to calculate it twice. Also sometimes you want the child component to be totally encapsulated, sometimes you want to drop a component down and you dont care what it does under the hood, the implementation detail shouldn't be leaked if you can help it.
Finally I think your examples aren't quite right.
show ?? <MyChildComponent/>
will render the child when show is null, otherwise it will render the value of show...Well, you're right with the cheapness of React components but you're missing the re-rendering of every single on of the components. If the parent component have a state that changes a lot, your ChildComponent will re-render too, now let's imagine that we have something like a form component that on every keypress will set a state, that will make you loose performance...the same happen if you have something like a graph that will set a lot of states...
On the second pointer, you can have a null validation on the conditional rendering but not on a render from a component, because you will have a useless lifecycle running, so there's no reason why a return (null) will be valid. Although, I did a little disclosure about sometimes you will have something like a wrapper that will not be showing but may still be needed, but still that wrapper will not return just NULL, at least children to be there...
On the error, yeah thanks! I totally miss that! I just fix it but not because the validation wasn't right but because the validation was returning
false
not null (also that's no good, but the example was explained it with null)It's a common practice but not a good practice, tho. And could have a little impact on performance depending on what you're doing, that's why I told that may potentially impact your performance. When managing big apps with a lot of functionalities, those extra re-renders will drop the performance of the app.
You can try it by using a ChildComponent and put a
console.log('re-render')
and you will see the multiple logs on the console. Now, imagine that you have it on a component like a form, that will set a new state on every key press and you have just 3 component that return null or false...It will do a lot of re-rendering just for 3 components with a bad conditional rendering.I return just an empty Fragment instead of null like this <></>
Interesting. Besides the point of the author tho. He argues we shouldn't do that either - because the component will still go through lifecycle before returning empty fragment. I'd love to know perf difference to returning null tho. My feeling is that it must be. But slower tho.
I'n not sure if Fragment also triggers a full lifecycle would be interesting to look into this a bit more.
I guess so. In the end, render is performed and returns something. And execution of the render is part of the lifecycle.
The point is that react must first evaluate the empty fragment to figure out "it's nothing" as opposed it was filled with children.
Yeah thinking more about it that makes sense. Need to go back and optimize 😫
Yeah bro, a empty fragment also trigger a full lifecycle because of the render of the fragment, as soon as you have it, then the component exist ergo, have its own lifecycle
The first part is good is we do condition && , that's the right way. And the post was mean to talk about a separate component that return null, because that will trigger a whole lifecycle that won't be doing anything.
I think It's a messy way and it violates SRP (also an implementation hiding). Components are self-sufficient modules. What if I should check browser features? Do some calculations? Request api for determine should I render a component content? It's cannot be done on a higher level. it will cause a huge performance penalty when render conditions will trigger sibling code to execute.
While this would make sense in a very small app, it seems as if it would make using data stores such as Redux or Mobx impossible. Components could not check the store to see if they have the data they need in order to render, but instead would be completely dependent, always, on props passed from a parent. This would require making the majority of components 'dumb' components that only render and do no logic of their own. In an enterprise-scale app this kind of prop drilling gets out of hand very quickly. It can also be difficult to maintain the codebase if I must always manually trace prop threads backwards through who knows how many layers, until I finally find where a value is being supplied or calculated.
Hi :)).
Thanks to an article.
I have error when I'm using typescript with react.
{Its return type 'false | Element' is not a valid JSX element.
Type 'false' is not assignable to type 'Element | null'.}
This error appears when I return {false} instead of null
When focusing on perf in a React app, I'd start by running a lighthouse audit, and seeing what your areas of improvement are. Address those, vs looking for micro-improvements than don't have a measurable impact.
Having the parent unnecessarily in charge of a child rendering can make it more difficult for a reader of your code to track down the state of the components. I also have never seen performance impacts (in a measurable way, which would make me consider refactoring) of having components returning null.
One thing being discounted here is readability. Lets assume you're working on a project with other developers, and your code gets handed off, what does this decision do to readability?
When concerns are separated, and components handle a single responsibility, they are definitely easier to debug. Think of it this way, if I am inheriting your code, and I am tracking down an issue related to a component, I am now going to have to touch more files to understand the state of the application. However, if components contain their own logic, and we've separated our concerns, then it's a one and done.
While you could establish patterns of having orchestration components that only manage conditional rendering, I think this design decision could be premature optimization at the cost of readability by sacrificing a separation of concerns.