Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

How to optimize React components for conditional rendering

Dive into the nuances of React component rendering. Find out which approach—parent decision or child decision—enhances your code's performance.
Tommy Groshong
|
February 3, 2020
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

A common situation when writing React components is needing to render a particular component only under certain conditions.

There are many ways to accomplish this, but the route you take often has nuanced implications or differing utility based on the current state of the code/component/system.

Let us consider the case of a component that requires some prop data to render. The following are two, general tactics for handling missing prop data:

  1. Have the parent decide not to render the child if the prop is unavailable
  2. Have the child itself decide to return nullish if a prop is unavailable

Tactic 1

const Child = ({requiredProp}) => {
  return <div className="some-class">{requiredProp}</div>
}

const App = (props) => {
  let importantData = useImportantData()
  return (
    <div>
      <Header />
      <Main />
      {importantData && <Child requiredProp={importantData} />}
      <Footer />
    </div>
  )
}

‍

Tactic 2

const Child = ({requiredProp}) => {
  if (!requiredProp) {
    return null
  }
  return <div className="some-class">{requiredProp}</div>
}

const App = (props) => {
  let importantData = useImportantData()
  return (
    <div>
      <Header />
      <Main />
      <Child requiredProp={importantData} />
      <Footer />
    </div>
  )
}

Note: I’m using “null checks” as the use case but this could easily apply to other kinds of validation, i.e. missing attributes, unsupported types, etc.

Both are valid approaches that are useful in varying cases. However, I find Tactic 2 (let child decide rendering) to be a better default approach.

Why I prefer Tactic 2

I find Tactic 2 preferable in most cases because it is better at (1) pushing rendering decisions down into more-relevant components and (2) encapsulating knowledge about a component and its internal needs.

The following are specific areas it performs better:

  1. Avoids extra boolean and ternary expressions
  2. Prevents reliance on weak PropType “guarantees”
  3. Avoids duplicating checks among many parents

Avoids extra boolean and ternary expressions

It can prevent boolean and ternary expressions a la {requiredProp && <Child prop={requiredProp} />} in our example. Like many programming constructs, they’re fine in small amounts but can get easily out of hand: (1) too many expressions, (2) nested expressions, or (3) overly large expressions. Also, these expressions have some annoying gotchas for JSX rendering because of falsiness in JavaScript: '' or 0 values for requiredProp would fail the && check and not render Child.

Expecting the Child component to deal with returning a nullish value itself if it doesn’t have the data it needs (1) simplifies the parent, (2) allows for more rigorous validation that may be clumsy in a simple expression, and (3) further encapsulates the knowledge of what a component needs to render.

Prevents reliance on weak PropType “guarantees”

Using PropTypes to denote a prop as required is simple:

Child.propTypes = {
  requireProp: PropTypes.object.isRequired()
}

However, this provides only a very weak guarantee with several problems:

  1. PropTypes failures only log to the console, so they are easy to miss
  2. PropTypes are commonly stripped from production builds
  3. The common penalty for using a nullish prop is raising a TypeError that breaks the whole render tree

Because of this, the safest approach is to expect a component to do its own validation regardless of what its parent does or its PropTypes “guarantee”. However, if you’re doing TypeScript, required props in interfaces are more useful because of the compiler checks.

Avoids duplicating checks among many parents

If some piece of data is not available in one parent component, it’s equally likely that it may not be available in another. So, every parent who renders the Child component must duplicate the validation done by Tactic 1. Obviously, this is not a big deal if the Child component is used in only one place, but who knows what next sprint will bring.

Case for Tactic 1

I have found Tactic 1 (let parent decide rendering) to be the better tactic in the following cases:

  1. Rendering modal-like things
  2. Rendering components that are known to have a costly setup or render (i.e., trigger lots of side effects, render large trees, or perform expensive calculations)

Be warned, the assumption of “costly” by the parent can easily be wrong or outdated. Maybe the Child intuitively seems costly but in actuality isn’t; maybe the Child was costly in the past but isn’t anymore. I find it best to start with Tactic 2 and then refactor to Tactic 1 if it becomes a problem; then it’s just another optimization decision.

Tactic 2B: the interrupting component

Tactic 2 is very useful for the average case, but as explained earlier some cases are not a natural fit. It can also be annoying if a component is using many React Hooks which, by the Rules of Hooks, need to always come before any if statements.

const Child = ({requiredProp}) => {
  // Will execute once even though we'd rather return early
  useEffect(() => {
    if (requiredProp) {
      doSomething(requiredProp)
    }
  }, [requiredProp])

  if (!requiredProp) {
    return null
  }
  return <div className="some-class">{requiredProp}</div>
}

Now this is fine for a few simple hooks, but if the hooks grow substantially in size, complexity, or number then a variation of Tactic 2 becomes enticing. I’ll denote the following variation as Tactic 2B. It fixes the problem with an intermediate component that interrupts the render flow:

const InnerChild = ({requiredProp}) => {
  useEffect(() => {
    doSomething(requiredProp)
  }, [requiredProp])
  return <div className="some-class">{requiredProp}</div>
}

const Child = (props) => {
  if (!props.requiredProp) {
    return null
  }
  return <InnerChild {...props} />
}

const App = (props) => {
  let importantData = useImportantData()
  return (
    <div>
      <Header />
      <Main />
      <Child requiredProp={importantData} />
      <Footer />
    </div>
  )
}

While this is overkill in our contrived example, it can be very powerful in complex, real-world cases and can actually be elegant across module boundaries where the API looks the same from the outside:

// Child.js

const Child = ({requiredProp}) => {
  useEffect(() => {
    doSomething(requiredProp)
  }, [requiredProp])
  return <div className="some-class">{requiredProp}</div>
}

const ChildInterrupt = (props) => {
  if (!props.requiredProp) {
    return null
  }
  return <Child {...props} />
}

export default ChildInterrupt

I suppose you could also accomplish this with a Higher-order Component (HOC), but I’m bad at writing those so I avoid writing them altogether.

Conclusion

As I said, both tactical approaches have their place depending on your situation. However, in many cases it’s best to push rendering decisions down into the components and encapsulate as much knowledge about them as you can. With that in mind, some variation of Tactic 2 is often the best default approach.

Related Insights

🔗
Effective React testing
🔗
Model View Controller pattern in React: A deep dive
🔗
Finding the right React component in the MUI design system

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.