DEV Community

Cover image for Cleaner React: Conditional Rendering
Matti Salokangas
Matti Salokangas

Posted on • Edited on • Originally published at codingzeal.com

Cleaner React: Conditional Rendering

Often times React components become hard to understand due to conditional rendering.  At first a simple if/else or ternary operator appears benign to your overall readability, but over time as changes occur another if/else or ternary may be added.

Compounding this issue is when conditional operators are nested many times, which unfortunately is too easy to do.

Let's first look at how to conditionally render in React and then dive into several experiments that might present more readable ways to conditionally render in React.

Conditional Rendering Styles

Simple Scenario

Scenario: Show a login component vs a register component given the property "isLoggedIn"

Using &&

Used quite often the "&&" is easy to throw in for some quick conditional logic.

const Session = ({ isLoggedIn }) => {  
 return (  
   <>  
     {isLoggedIn && <Login />}  
     {!isLoggedIn && <SignOut />}  
   </>  
 );  
};  
Enter fullscreen mode Exit fullscreen mode

Using If/Else Statements

Given this simple scenario, a guard clause works here and is a bit more readable than the "&&".

const Session = ({ isLoggedIn }) => {  
 if (isLoggedIn) {  
   return <SignOut />  
 }  

 return <Login />  
};  
Enter fullscreen mode Exit fullscreen mode

Using Ternary Operator

This also is easier to understand; being able to one line this is pretty nice.


const Session = ({ isLoggedIn }) => isLoggedIn ? <SignOut /> : <Login />;  
Enter fullscreen mode Exit fullscreen mode

Complex Scenario

Scenario: Show a login component vs a register component given the property "isLoggedIn", in addition show the "UnicornLogin" component if the "isUnicorn" flag is true.

Using &&

This is awful. It is clear that the "&&" is only good to use sparingly and only when there is one condition.

const Session = ({ isLoggedIn, isUnicorn }) => {  
 <>  
   {isLoggedIn && !isUnicorn && <Login />}  
   {!isLoggedIn && isUnicorn && <isUnicorn />}  
   {!isLoggedIn && !isUnicorn && <SignOut />}  
 </>;  
};  
Enter fullscreen mode Exit fullscreen mode

Using If/Else Statements

Less awful, but this is going to make things tricky if you ever wanted to wrap each of the components being returned with another component.

const Session = ({ isLoggedIn, isUnicorn }) => {  
 if (isLoggedIn) {  
   return <SignOut />;  
 } else if (isUnicorn) {  
   return <UnicornLogin />;  
 };

 return <Login />;  
};  
Enter fullscreen mode Exit fullscreen mode

Using Ternary Operator

Also, less awful, yet prone to the same struggles as using if/else statements when more changes inevitably occur.

const Session = ({ isLoggedIn, isUnicorn }) => {  
 if (isLoggedIn) {  
   return <SignOut />;  
 }

 return isUnicorn ? <UnicornLogin /> : <Login />;  
};  
Enter fullscreen mode Exit fullscreen mode

Conditional Gotchas

Now that we've seen how to use conditional rendering in React, let's take a look at some specific examples where conditional rendering may not do what you expect.

Logical "&&" Operator

Rendering a 0 when mapping

When using "&&" to check the length of a data set and then mapping over it will render "0" when that list is empty.

This was recently highlighted by Kent C. Dodds in his article https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx

The code below will render "0" when data is empty.

const RenderData = ({ data }) =>  data.length && data.map(...);  
Enter fullscreen mode Exit fullscreen mode

This can be resolved by either using a ternary operator instead of "&&".

const RenderData = ({ data }) =>  data.length > 0 ? data.map(...) : null;  
Enter fullscreen mode Exit fullscreen mode

This can also be resolved by either using an if/else statement.

const RenderData = ({ data }) =>  data.length > 0 ? data.map(.const RenderData = ({ data }) => {  
 if (data.length === 0) return null;

 return data.map(...)  
}  
Enter fullscreen mode Exit fullscreen mode

Rendering a 0 in a component

This React Native specific, but when conditionally rendering a component and passing in your condition you may inadvertently render a 0. This results in your app crashing and the following error message: "Invariant Violation: Text strings must be rendered within a component.".

This will crash your app if "message" is ever 0:

message && <Text>{message}</Text>  
Enter fullscreen mode Exit fullscreen mode

Nested Ternaries

Ternaries are nice if you have one condition. However, it is too easy to not refactor and quickly add another check and then another.

This is a simple example, you can imagine what this would look like if each component we rendered were 5-10 or more lines long.

const RenderData = ({ data }) => {  
 return data.length === 0 ? null  
   : data.length === 1  
   ? <SingleItem data={data} />  
   : data.length === 2  
   ? <DoubleItem data={data} />  
   : <MultiItem data={data} />  
}  
Enter fullscreen mode Exit fullscreen mode

Writing Better Conditions

We've taken a look at how to write basic conditional statements in React, along with some pitfalls to avoid. Let's consider how we can write better conditional code in React.

Conditional Operator Component

I think it is easier to read JSX if your brain only has to parse and not conditional statements plus . So, how can we write conditional operators as XML?

Let's consider creating a component called "RenderIf" and takes a boolean property of "isTrue" and renders its children.

export const RenderIf = ({ children, isTrue }) => isTrue ? children : null;

RenderIf.propTypes = {  
 children: node.isRequired,  
 isTrue: bool.isRequired,  
};  
Enter fullscreen mode Exit fullscreen mode

Re-writing our example with the "RenderIf" component, I'm mostly looking at XML. However, there is still some boolean logic that could be cleaned up.

const RenderData = ({ data }) => {  
 return (  
   <>  
     <RenderIf isTrue={data.length === 1}>  
       <SingleItem data={data} />  
     </RenderIf>  
     <RenderIf isTrue={data.length === 2}>  
       <DoubleItem data={data} />  
     </RenderIf>  
     <RenderIf isTrue={data.length > 2}>  
       <MultiItem data={data} />  
     </RenderIf>  
   </>  
 );  
}  
Enter fullscreen mode Exit fullscreen mode

We can clean up the boolean logic by wrapping our "RenderIf" component.

const IfSingleItem = ({ children, data }) => <RenderIf isTrue={data.length === 1}>{children}</RenderIf>  
const IfDoubleItem = ({ children, data }) => <RenderIf isTrue={data.length === 2}>{children}</RenderIf>  
const IfMultiItem = ({ children, data }) => <RenderIf isTrue={data.length > 3}>{children}</RenderIf>

const RenderData = ({ data }) => {  
 return (  
   <>  
     <IfSingleItem data={data}>  
       <SingleItem data={data} />  
     </IfSingleItem>  
     <IfDoubleItem data={data}>  
       <DoubleItem data={data} />  
     </IfDoubleItem>  
     <IfMultiItem data={data}>  
       <MultiItem data={data} />  
     </IfMultiItem>  
   </>  
 );  
}  
Enter fullscreen mode Exit fullscreen mode

Having our Cake and Eating it too!

I personally like reading more declarative React, however one of the pitfalls that I had not mentioned is that the children for the RenderIf component will still go through a render cycle even if our condition is false. This is because RenderIf is still JSX vs being straight javascript.

So, how do we get around this?

I happen to write a RenderIf Babel plugin that does just this! You can find the code out on my GitHub here.

Essentially, this plugin will take code that looks like this:

<RenderIf isTrue={someCondition}>
  <span>I am the children</span>
</RenderIf>
Enter fullscreen mode Exit fullscreen mode

and turn it into this:

{someCondition ? <span>I am the children</span> : null
Enter fullscreen mode Exit fullscreen mode

So, we are getting our declarative syntax and when it is transpiled, we get the more performant version.  Also, if you use this plugin you won't have to write your own RenderIf component! 🎉

When to Refactor

Often times if there is an accumulation of complexity in a component it is a sign that there are components that should be refactored out. Although knowing exactly when and how to refactor is hard to know, here are some general rules of thumb that you might consider.

100+ Lines of Code

Keep components to less than 100 lines. As you start to get into the 100-250 line territory you should really start thinking about refactoring. If you are at 500+ lines of code, that should be refactored as soon as possible.

High Cyclomatic Complexity

Cyclomatic complexity is the number of paths through your code. So, if you have a simple if/else block, then it has a cyclomatic complexity of 2, where as if you had a block of if/else if/else if/else if/else, the cyclomatic complexity would be 5.

You can enforce this by using the ESLint complexity rule

It is up to you what level of complexity is appropriate, but somewhere around 4-5 is usually a good place to start.

Cleaner React

We can write cleaner React by extracting out distracting syntax and knowing when to refactor.

Creating a helper component like "RenderIf" is an example in how you could extract out conditional logic into a declarative syntax. This makes it a little bit easier for your brain since it is mostly taking in XML. Building on that idea, we can wrap our helper component to create a richer set of conditional components that add even more context.

At the end of the day, a component that is large and complex, no matter how clean the React is, will be prone to bugs and simply not fun to work on. It is a good practice to know when to refactor and to be disciplined to do that refactor when you know it needs to happen.

Happy coding and keep your React clean!

Originally posted on ZEAL's blog here

Top comments (18)

Collapse
 
rtivital profile image
Vitaly Rtishchev
const IfSingleItem = ({ children, data }) => <RenderIf isTrue={data.length === 1}>{children}</RenderIf>  
const IfDoubleItem = ({ children, data }) => <RenderIf isTrue={data.length === 2}>{children}</RenderIf>  
const IfMultiItem = ({ children, data }) => <RenderIf isTrue={data.length > 3}>{children}</RenderIf>

const RenderData = ({ data }) => {  
 return (  
   <>  
     <IfSingleItem data={data}>  
       <SingleItem data={data} />  
     </IfSingleItem>  
     <IfDoubleItem data={data}>  
       <DoubleItem data={data} />  
     </IfDoubleItem>  
     <IfMultiItem data={data}>  
       <MultiItem data={data} />  
     </IfMultiItem>  
   </>  
 );  
}  
Enter fullscreen mode Exit fullscreen mode

This looks really nasty. It's always better to such keep logic out of jsx. We have a switch operator for that.

Collapse
 
sturdynut profile image
Matti Salokangas

I see what you are saying; you could very well use a switch operator as well. The concept here was to demonstrate how you can wrap RenderIf to create a richer API for your JSX.

Collapse
 
rtivital profile image
Vitaly Rtishchev

What I'm saying is that RenderIf is a huge mess and should not be used

Thread Thread
 
sturdynut profile image
Matti Salokangas • Edited

I've used it on several projects where teams were really happy with it. I'm curious why you think it is a "huge" mess considering the alternatives and potential pitfalls?

Collapse
 
hoverbaum profile image
Hendrik

Cool idea. I think components like your RenderIf can help convey what you are doing. At least in complex scenarios, they can be easier to reason about than complex statements. Thus, gathering experience as to when to do this kind of refactoring is important, but a nice inspiration.

Collapse
 
alexkhismatulin profile image
Alex Khismatulin

There are also babel JSX extensions that allow conditional rendering like the following ones
npmjs.com/package/babel-plugin-jsx...
npmjs.com/package/babel-plugin-jsx...

Collapse
 
sturdynut profile image
Matti Salokangas

That is really nice! I think the one caveat here is that it is harder to discern the code paths in your components since the condition is nested in your component vs physically part of your component tree. I find it easier to see using RenderIf or something similar which also makes it easier for me to see the various conditions I need to test.

Collapse
 
alexkhismatulin profile image
Alex Khismatulin

100% agreed, but that’s still an option

Collapse
 
ramblingenzyme profile image
Satvik Sharma

I like the idea of pushing your conditions down your React tree wherever possible, so like passing isUnicorndown into the Login component and handling that condition internally.

Collapse
 
sturdynut profile image
Matti Salokangas

That works too. But then you are mixing presentation logic in with your component code vs having only what you need in your component. I think having your presentation logic outside of the component makes things easier to reason about and make sure that components aren't doing more than they should.

Collapse
 
ramblingenzyme profile image
Satvik Sharma • Edited

Not sure I agree, the whole point of props is to change what's rendered, from the user's perspective the difference is probably "the login is themed differently when I'm a unicorn", so what's the meaningful difference between, swapping a whole component in this level, doing the swap inside or changing styles internally, so why does it matter that it's handled outside the component or in it? It sounds to me like exactly part of the component's responsibility.

Also, turning JSX into a logic heavy DSL is not my jam at all.

Thread Thread
 
sturdynut profile image
Matti Salokangas

That is true. But, something still needs to conditionally render based on those prop changes. I've outlined many ways to do that in this post and suggested a way that I prefer using RenderIf. If it is not your thang, that's cool. Readability is subjective to some degree. I personally find it easier to take in JSX that looks more like HTML than a mix of HTML and JS if/else/switch statements.

Thread Thread
 
ramblingenzyme profile image
Satvik Sharma

For sure, which is why most of the time I prefer calculating as much as possible in plain JS so the JSX is mostly just JSX, even if it's just putting the ternaries into constants

Thread Thread
 
sturdynut profile image
Matti Salokangas

Totally. You can clean up a ton of JSX by extracting variables and doing calculations prior to rendering. That is a great point and I find myself doing those refactors when I'm trying to make components more readable.

Collapse
 
jacobmgevans profile image
Jacob Evans

XState and React Query I think helps a lot with this overhead. I loved the explanations super detailed and learned a lot about cyclomatic complexity.

Collapse
 
xavierbrinonecs profile image
Xavier Brinon

Amen

Collapse
 
johnsoncherian profile image
Johnson • Edited

The code below will render "0" when data is empty.
const RenderData = ({ data }) => data.length && data.map(...);

What about this.?
const RenderData = ({ data }) => (!!data.length) && data.map(...);

Collapse
 
sturdynut profile image
Matti Salokangas

That would return false. I think the trap here is that it is easy to forget to cast the data.length to a boolean type.