DEV Community

loading...
Cover image for Stop returning NULL components

Stop returning NULL components

Daniel Silva
I'm a React/React Native developer since 3 years ago. I'm a JavaScript enthusiast (Because there are not enough…)
Updated on ・3 min read

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;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 ;)

Discussion (29)

Collapse
sergey profile image
sergeyv

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?

Collapse
sergey profile image
sergeyv • Edited

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.

Collapse
asyrialak profile image
Andrew Syriala

Does it affect the lighthouse score in any measurable way?

Thread Thread
sergey profile image
sergeyv

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.

Thread Thread
asyrialak profile image
Andrew Syriala

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.

Collapse
ucvdesh profile image
Daniel Silva Author

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...

Thread Thread
sergey profile image
sergeyv

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.

Collapse
jackmellis profile image
Jack • Edited

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...

Collapse
ucvdesh profile image
Daniel Silva Author

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)

Collapse
codyseibert profile image
Cody Seibert

Do you happen to have benchmarks that show how much this really affects performance?

Collapse
ucvdesh profile image
Daniel Silva Author

That'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...

Collapse
ucvdesh profile image
Daniel Silva Author

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!

Collapse
codyseibert profile image
Cody Seibert • Edited

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.

Thread Thread
ucvdesh profile image
Daniel Silva Author

Exactly! This is a correct conditional render, because MyComponent does not render unless myBoolean is true

Collapse
ucvdesh profile image
Daniel Silva Author

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.

Collapse
ivanjeremic profile image
Ivan Jeremic • Edited

I return just an empty Fragment instead of null like this <></>

Collapse
loopmode profile image
Jovica Aleksic

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.

Collapse
ivanjeremic profile image
Ivan Jeremic • Edited

I'n not sure if Fragment also triggers a full lifecycle would be interesting to look into this a bit more.

Thread Thread
loopmode profile image
Jovica Aleksic

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.

Thread Thread
ivanjeremic profile image
Ivan Jeremic

Yeah thinking more about it that makes sense. Need to go back and optimize 😫

Thread Thread
ucvdesh profile image
Daniel Silva Author

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

ucvdesh profile image
Daniel Silva Author

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

ucvdesh profile image
Daniel Silva Author

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.

Collapse
asyrialak profile image
Andrew Syriala

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.

Collapse
vikkio88 profile image
Vincenzo

I dont know if it is just me but this sentence

That is a mistake that can potentially increase a lot the performance of your application.

seems to mean exactly the opposite of what you mean?

Collapse
ucvdesh profile image
Daniel Silva Author

hahahahaha you're right! Sorry for the mistake!