DEV Community

Cover image for Stop returning NULL components
Daniel Silva
Daniel Silva

Posted on • Edited on

Stop returning NULL components

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

Latest comments (30)

Collapse
 
hayk1997 profile image
Hayk-1997

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

Collapse
 
sevenzark profile image
Dave M

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.

Collapse
 
bharati21 profile image
Bharati Subramanian

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?

Collapse
 
lffate profile image
LFFATE

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.

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.

 
ucvdesh profile image
Daniel Silva

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.

 
ucvdesh profile image
Daniel Silva

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

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

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
 
ucvdesh profile image
Daniel Silva

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

hahahahaha you're right! Sorry for the mistake!