There are lots of different ways to conditionally render components in React: if … else, early returns, ternaries, logical ANDs (only to name a few).
It’s obviously important that you understand the most common ones. But it’s also important to know how and when each of them can help you to write cleaner code. For example, the ternary operator might be popular. But as soon as you use multiple conditions and chain ternaries together you’re in for a spaghetti feast.
On this page, you can learn the most common ways to conditionally render components in React. Not only that. You can also learn about different situations when a certain approach can be dangerous while another one can lead to clean code.
My personal favorite is “returning early and often”. Instead of using conditionals inside your JSX you can use simple if
statements.
function ListPage() {const { data, error, isLoading } = useGetData();if (isLoading) {return (<Layout><LoadingSpinner /></Layout>);}if (error) {return (<Layout><ErrorMessage message={error.message} /></Layout>);}return (<Layout>{data.items.map((item) => (<Item key={item.id} item={item} />))}</Layout>);}
There might be a bit of code duplication (in the above example the repeated <Layout>
wrapper). But early returns make the program flow easy to follow. For example, if there’s a bug with the loading screen you can likely stop reading at the very first if
statement and ignore the rest of the code.
Note that you don’t need an else
statement. This saves you one indentation level in the last return
. This again makes the JSX easier to read especially if it’s deeply nested.
In some cases, early return statements may not seem possible. For example, the components that should be rendered conditionally may have a lot of sibling components:
function ListPage() {const { data, isLoading } = useGetData();if (isLoading) {return (<Layout><LoadingSpinner /><MoreComponents>{//... more components here}</MoreComponents></Layout>);}return (<Layout>{data.items.map((item) => (<Item key={item.id} item={item} />))}<MoreComponents>{//... more components here}</MoreComponents></Layout>);}
In this case, it’s tedious and error-prone to duplicate the sibling <MoreComponents>
and its children.
But often we can create a separate component and use early returns there.
export function ItemList() {const { data, error, isLoading } = useGetData();if (isLoading) {return (<LoadingSpinner />);}return data.items.map((item) => <Item key={item.id} item={item} />);}export function ListPage() {return (<Layout><ItemList /><MoreComponents>{//... more components here}</MoreComponents></Layout>);}
When you want to render a list only if an array exists the optional chaining operator ?
is a great fit.
Let’s assume we render a blog post that may or may not have a list of tags. If it has tags we show them. If not we simply don’t render anything.
function ListPage() {// data.tags may be null or undefinedconst { data } = useGetData();return (<Layout><Post post={data.post} />{data.tags?.map((tag) => (<Tag key={tag.id} tags={tag} />))}</Layout>);}
If data.tags
is null
or undefined
React renders undefined
(aka nothing) otherwise it renders the tags. Translated into code this means.
// data.tags is null or undefined<Layout><Post post={data.post} />{undefined}</Layout>// data.tags defined<Layout><Post post={data.post} /><Tag key="tag1" tags={tag1} /><Tag key="tag2" tags={tag2} /><Tag key="tag3" tags={tag3} /></Layout>
When you have a simple “if … else” situation with a single condition, a ternary operator can be a good fit.
function ListPage() {const { data, isLoading } = useGetData();return (<Layout>{isLoading ? (<LoadingSpinner />) : (data.items.map((item) => <Item key={item.id} item={item} />))}</Layout>);
This is quite easy to read if formatted well (as Prettier did above) and the return values are short. If the inner JSX gets more complex the ternary starts to become hard to read.
function ListPage() {const { data, isLoading } = useGetData();return (<Layout>{isLoading ? (<Loading>{// lots of JSX here}</Loading>) : (<Content>{// lots of JSX here}</Content>)}</Layout>);
Ternaries also become hard to read you have a “if … else if … else” situation. Now you need to chain the ternary operators which can be very hard to follow.
Here is a relatively simple example where it’s already easy to miss the second condition.
function ListPage() {const { data, isLoading, error } = useGetData();return (<Layout>{isLoading ? (<LoadingSpinner />) : error ? (<ErrorMessage message={error.message} />) : (data.items.map((item) => <Item key={item.id} item={item} />))}</Layout>);}
In this situation, it’s better to use early returns or the logical AND operator as you’ll see in a bit.
When you want to render a component if a condition is met and nothing instead the logical AND operator &&
is a good fit.
function ListPage() {const { data, error } = useGetData();return (<Layout>{error && <ErrorMessage message={error.message} />}{data.items.map((item) => (<Item key={item.id} item={item} />))}</Layout>);}
Note that some falsy values are rendered by React. In this example, you will either see the list of items as expected or the number 0
.
function ListPage() {const { data, error } = useGetData();return (<Layout>{data.items.length && data.items.map((item) => (<Item key={item.id} item={item} />))}</Layout>);}
So in doubt, you should either
!!data.items.length
data.items.length > 0
Boolean(error)
.A problem you can often see when using the logical AND operator (as well as ternary operators) is chained conditions. Look how awful this is to read even though this is still a quite simple example.
function ListPage() {const { data, isLoading, error } = useGetData();return (<Layout>{isLoading && <LoadingSpinner />}{!isLoading && !!error && <ErrorMessage message={error.message} />}{!isLoading && !error && !!data && data.items.map((item) => (<Item key={item.id} item={item} />))}</Layout>);}
To improve the readability of this code we can extract the chained conditions into variables. Now the conditions inside the JSX are short. The reader doesn’t have to follow what exactly showData
means to understand the code. But they’re free to investigate the condition if necessary.
This is also the alternative to chained ternary operators mentioned in the section above.
function ListPage() {const { data, isLoading, error } = useGetData();const showError = !isLoading && !!error;const showData = !isLoading && !error && !!data;return (<Layout>{isLoading && <LoadingSpinner />}{showError && <ErrorMessage message={error.message} />}{showData && data.items.map((item) => (<Item key={item.id} item={item} />))}</Layout>);}
When you want to hide a component based on a condition returning null
can be a good alternative to the logical AND operator &&
.
We can either let the parent decide to hide the component with &&
.
export function ItemList({ items }) {return items.map((item) => <Item key={item.id} item={item} />);}function ListPage() {const { data } = useGetData();return (<Layout>{!!data?.items && <ItemList items={data.items} />}</Layout>);}
Or we make the hiding the responsibility of the child component.
export function ItemList({ items }) {if (!items) {return null;}return items.map((item) => <Item key={item.id} item={item} />);}function ListPage() {const { data } = useGetData();return (<Layout><ItemList items={data?.items} /></Layout>);}
Whether to use the &&
operator or return null
is a case-to-case decision. The question is whether it should be the parent’s or the component’s own responsibility to render based on the condition.
Note that returning null
from the child can also have an impact on performance. If the child contains a lot of logic or sends API requests the following code wouldn’t be ideal.
export function ItemList({ isHidden }) {const { data } = useGetData();if (isHidden) {return null;}return data.items.map((item) => <Item key={item.id} item={item} />);}function ListPage({ isItemListHidden }) {return (<Layout><ItemList isHidden={isItemListHidden} /></Layout>);}
Since we can only return
from the component after all hooks we would send an API request even though the component isn’t visible. Which probably is not what we want.
Another alternative is assigning components to variables and adding them to the final JSX. You can use different styles. Here is my favorite as it supports multiple conditions and is relatively easy to read.
function ListPage() {const { data, isLoading } = useGetData();let content = <LoadingSpinner />if (!isLoading) (content = data.items.map((item) => <Item key={item.id} item={item} />));return <Layout>{content}</Layout>;}
Obviously, you can also use other options like a ternary operator.
function ListPage() {const { data, isLoading } = useGetData();const content = isLoading ? (<LoadingSpinner />) : (data.items.map((item) => <Item key={item.id} item={item} />));return <Layout>{content}</Layout>;}
I’m personally not a big fan of these render variables. From my perspective, they encourage the mixing of business logic with UI code. When you start reading the component from top to bottom you’re like “Hey here’s the business logic, wait there’s a bit of JSX, and again business logic, and again JSX…”.
If you have a condition based on e.g. a string you might also use an enum object. You can use the possible values as keys of an object and the corresponding components as their values.
const components = {"loading": <LoadingSpinner />,"error": <ErrorMessage />,"success": <ItemList />}function ListPage() {const { status } = useGetData();return <Layout>{components[status]}</Layout>;}
This gets a bit more verbose when you want to pass props to the components.
const components = {"loading": () => <LoadingSpinner />,"error": ({ error }) => <ErrorMessage message={error.message} />,"success": ({ data }) => <ItemList items={data.items} />}function ListPage() {const { data, error, status } = useGetData();return <Layout>{components[status]({ data, error })}</Layout>;}
Honestly, I’ve never used this nor have I seen it out in the wild. But I wanted to share this for completion.