Use a decorator to conditionally render React components

Shaun Gallagher
4 min readAug 17, 2017

--

Here’s a common pattern that you might notice when developing React components:

  1. The component requires some data, so it (or its parent) initiates an API request for that data.
  2. Until that response is received, the component displays a loading indicator.
  3. Once the response is received, the component displays content that makes use of that data.

When you make components that follow this pattern, you end up with a lot of this:

render() {
const { requiredData } = this.state;
if (requiredData) {
return <div>Display {requiredData}</div>;
} else {
return <LoadingIndicator />;
}
}

Sometimes, though, you might want to display a loading indicator for both an initial loading state and a refreshing or reloading state.

In the former case, you don’t have any existing data. But in the latter case, you have stale data and might not want to hide it entirely. You just want to indicate to the user that it’s being refreshed. So you might create a loading-state wrapper component that shows an opaque loading indicator during the initial loading state, but that overlays a semi-opaque loading indicator on top of the stale data during the reloading state:

render() {
const { isLoading, isReloading, requiredData } = this.state;
return (
<LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
>
<div>Display {requiredData}</div>
</LoadingWrapper>
);
}

Avoiding key errors

But wait, what if requiredData is not a string but an object, and you need to go several levels deep to access one of its properties?

render() {
const { isLoading, isReloading, requiredData } = this.state;
return (
<LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
>
<div>Display {requiredData.may.not.be.present}</div>
</LoadingWrapper>
);
}

Until your initial API request returns a response, requiredData is going to be null, and you’re going to get a key error when trying to access requiredData.may.not.be.present.

There are ways to get around this. For instance, you might use lodash to set default values:

render() {
const { isLoading, isReloading, requiredData } = this.state;
return (
<LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
>
<div>Display {_.get(requiredData, 'may.not.be.present', ''}</div>
</LoadingWrapper>
);
}

But when you have a lot of values like that, it becomes very messy.

Alternatively, you might put all of your content in a conditionally rendered block:

render() {
const { isLoading, isReloading, requiredData } = this.state;
return (
<LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
>
{requiredData &&
<div>Display {requiredData.may.not.be.present}</div>
}
</LoadingWrapper>
);
}

But now there’s a lot of scaffolding around your content, and it becomes tedious when you have to employ this scaffolding for a lot of components.

Is it possible to put all that logic into the LoadingWrapper component?

Unfortunately, not in a robust way. Child components render before their parent components, so by the time LoadingWrapper renders, you will have already hit the requiredData.may.not.be.present key error. (You might be able to hack its componentShouldUpdate method to short-circuit rendering in the case of isReloading, but the componentShouldUpdate method doesn’t get called on initial render, so it doesn’t help for theisLoading state.)

Fortunately, there is a way to remove some of that scaffolding and avoid the key errors.

The decorator solution

Decorators are a type of higher-order function. They accept a function as an argument, extend that function in some way, and return the modified function.

So, let’s apply a decorator to a component’s render method that handles all of that state management and conditional rendering.

A method decorator is a function with three arguments: target, key, and descriptor. In React, the target is the component class itself, and the descriptor.value is the method that is being decorated.

Here is a simple decorator that employs the same basic scaffolding as shown above:

const loadingManager = (target, key, descriptor) => {
// Store the original render method on the target.
target.renderOnLoad = target.renderOnLoad || descriptor.value;
descriptor.value = function () {
// bind `this` so the render function
// can access the original state
const render = target.renderOnLoad.bind(this);
const { isLoading, isReloading } = this.state;
const contents = isLoading ? null : render();
return <LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
>
{contents}
</LoadingWrapper>;
};
return descriptor;
};

Note that this decorator handles both the isLoading state, where the render method should not be invoked, and the isReloading state, where the render method should be invoked (but wrapped in the LoadingWrapper).

And here is how you would apply it to your component’s render method:

@loadingManager
render () {
const { requiredData } = this.state;
return <div>Display {requiredData.may.not.be.present}</div>;
}

Passing configuration to the decorator

If you need to go beyond a simple implementation — for instance, by setting the size of the loading indicator depending on the size of the component — you might need to pass some additional configuration to the decorator.

Fortunately, method decorators can accept arguments:

const loadingManager = (config) => (target, key, descriptor) => {
// Store the original render method on the target.
target.renderOnLoad = target.renderOnLoad || descriptor.value;
descriptor.value = function () {
// bind `this` so the render function
// can access the original state
const render = target.renderOnLoad.bind(this);
const { isLoading, isReloading } = this.state;
const contents = isLoading ? null : render();
return <LoadingWrapper
isLoading={isLoading}
isReloading={isReloading}
{...config}
>
{contents}
</LoadingWrapper>;
};
return descriptor;
};

Here is how you would apply it to your component’s render method:

@loadingManager({largeLoadingIndicator: false})
render () {
const { requiredData } = this.state;
return <div>Display {requiredData.may.not.be.present}</div>;
}

Now you can add largeLoadingIndicator as a prop in your LoadingWrapper component and style appropriately!

--

--

Shaun Gallagher

Author of ″Experimenting With Babies″ (ExperimentingWithBabies.com), "Experiments for Newlyweds" (Newlywed.science), and ″Correlated″ (Correlated.org).