React Design Patterns and Best Practices(Second Edition)
上QQ阅读APP看书,第一时间看更新

Conditionals

Things get more interesting when we start working with conditionals, for example, if we want to render some components only when certain conditions are matched. The fact that we can use JavaScript in our conditions is a big plus, but there are many different ways to express conditions in JSX, and it is important to understand the benefits and problems of each one of these to write code that is both readable and maintainable.

Suppose we want to show a logout button only if the user is currently logged into our application.

A simple snippet to start with is as follows:

  let button;

if (isLoggedIn) {
button = <LogoutButton />;
}

return <div>{button}</div>;

This works, but it is not very readable, especially if there are multiple components and multiple conditions.

In JSX, we can use an inline condition:

  <div> 
{isLoggedIn && <LoginButton />}
</div>

This works because if the condition is false, nothing gets rendered, but if the condition is true, the createElement function of the LoginButton gets called, and the element is returned to compose the resulting tree.

If the condition has an alternative (the classic if...else statement), and we want, for example, to show a logout button if the user is logged in and a login button otherwise, we can use JavaScript's if...else statement as follows:

  let button;

if (isLoggedIn) {
button = <LogoutButton />;
} else {
button = <LoginButton />;
}

return <div>{button}</div>;

Alternatively, and better still, we can use a ternary condition that makes the code more compact:

  <div> 
{isLoggedIn ? <LogoutButton /> : <LoginButton />}
</div>

You can find the ternary condition used in popular repositories, such as the Redux real-world example (https://github.com/reactjs/redux/blob/master/examples/real-world/src/components/List.js#L28), where the ternary is used to show a loading label if the component is fetching the data, or load more inside a button depending on the value of the isFetching variable:

  <button [...]> 
{isFetching ? 'Loading...' : 'Load More'}
</button>

Let's now look at the best solution for when things get more complicated and, for example, we have to check more than one variable to determine whether to render a component or not:

  <div>
{dataIsReady && (isAdmin || userHasPermissions) &&
<SecretData />
}
</div>

In this case, it is clear that using the inline condition is a good solution, but the readability is strongly impacted. Instead, we can create a helper function inside our component and use it in JSX to verify the condition:

  canShowSecretData() { 
const { dataIsReady, isAdmin, userHasPermissions } = this.props;
return dataIsReady && (isAdmin || userHasPermissions);
}

return (
<div>
{this.canShowSecretData() && <SecretData />}
</div>
);

As you can see, this change makes the code more readable and the condition more explicit. If you look at this code in six months, you will still find it clear just by reading the name of the function.

If you do not like using functions, you can use an object's getters, which makes the code more elegant.

For example, instead of declaring a function, we define a getter as follows:

  get canShowSecretData() { 
const { dataIsReady, isAdmin, userHasPermissions } = this.props;
return dataIsReady && (isAdmin || userHasPermissions);
}

return (
<div>
{this.canShowSecretData() && <SecretData />}
</div>
);

The same applies to computed properties. Suppose you have two single properties for currency and value. Instead of creating the price string inside your render method, you can create a class function:

  getPrice() { 
return `${this.props.currency}${this.props.value}`;
}

return <div>{this.getPrice()}</div>;

This is better because it is isolated and you can easily test it in case it contains logic.

Alternatively, you can go a step further and, as we have just seen, use getters:

  get price() { 
return `${this.props.currency}${this.props.value}`;
}

return <div>{this.price()}</div>;

Going back to conditional statements, other solutions require using external dependencies. A good practice is to avoid external dependencies as much as we can to keep our bundle smaller, but it may be worth it in this particular case, because improving the readability of our templates is a big win.

The first solution is render-if, which we can install with the following:

  npm install --save render-if

We can then easily use it in our projects, as follows:

  const { dataIsReady, isAdmin, userHasPermissions } = this.props;

const canShowSecretData = renderIf(
dataIsReady && (isAdmin || userHasPermissions)
);

return (
<div>
{canShowSecretData(<SecretData />)}
</div>
);

We wrap our conditions inside the renderIf function.

The utility function that gets returned can be used as a function that receives the JSX markup to be shown when the condition is true.

One goal we should always keep in mind is to never add too much logic inside our components. Some of them will require a bit of it, but we should try to keep them as simple as possible so that we can easily spot and fix errors.

We should at least try to keep the renderIf method as clean as possibleand, to do that, we can use another utility library called react-only-if, which lets us write our components as if the condition is always true by setting the conditional function using a higher-order component (HoC).

We will talk about HoC extensively in Chapter 4, Compose All the Things, but, for now, you need to know that they are functions that receive a component and return an enhanced one by adding some properties or modifying its behavior.

To use the library, we need to install it as follows:

  npm install --save react-only-if

Once it is installed, we can use it in our apps in the following way:

  import onlyIf from 'react-only-if';

const SecretDataOnlyIf = onlyIf(
({ dataIsReady, isAdmin, userHasPermissions }) => dataIsReady &&
(isAdmin || userHasPermissions)
)(SecretData);

return (
<div>
<SecretDataOnlyIf
dataIsReady={...}
isAdmin={...}
userHasPermissions={...}
/>
</div>
);

As you can see here, there is no logic at all inside the component itself.

We pass the condition as the first parameter of the onlyIf function, and when the condition is matched, the component is rendered.

The function used to validate the condition receives the props, state, and context of the component.

In this way, we avoid polluting our component with conditionals so that it is easier to understand and reason about.