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 ;)
Latest comments (30)
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
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.
Good article!
Wanted to know if there's an actual way to measure the performance when you use nullish coalescing/ &&/ return null ?
Also, I wonder why the docs have mentioned returning null, when it seems like a bad practice. Do you think it has any practical use cases?
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.
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.
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.
Kinda hard not to have any conditional render on an app as we have different profiles, permissions and states. So as we can manage some ways to avoid some of those, we still gotta use good practices as we develop
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.
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 dont know if it is just me but this sentence
seems to mean exactly the opposite of what you mean?
hahahahaha you're right! Sorry for the mistake!