Introduction
In this article, I'll start with a seemingly simple problem that led me to explore some more complex avenues of thought than I expected. Rather than directly providing you with the final solution, I wanted to share my journey and highlight these different points.
Note: This article was originally written in French and translated to English using Gemini and Claude, as it's easier for me to write in my native language.
You can find the complete code in this GitHub repository:
https://github.com/krambono/show
The Problem
When I use React (and JSX), I don't like conditional renderings of this kind:
condition && <Component />
condition ? <Component /> : null
It's purely subjective, but I find that it makes the code less readable.
Here's an example you'll likely recognize:
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Layout>
{isLoading && <p>Loading post...</p>}
{error ? (
<>
<h2 style={{ color: 'red' }}>Error</h2>
<p>Unable to retrieve post.</p>
<pre>{error.message}</pre>
</>
) : null}
{data && (
<article>
<h1>{data.title}</h1>
<p>{data.content}</p>
</article>
)}
</Layout>
);
};
I find the syntax heavy and not very explicit. It doesn't clearly highlight the condition and its result. You have to follow the chain of braces or parentheses to fully understand what's included and what isn't.
For example, I have a clear preference for Svelte or Angular syntaxes that offer if - else blocks in their templates.
Let's Create a Simple Component
A first solution to improve this display is to create a Show component that takes a condition
and JSX children as props, which will be displayed based on the condition.
type Props = {
condition: unknown;
children: ReactNode;
};
const Show: FC<Props> = ({ condition, children }) =>
condition ? children : null;
Note: The type of condition is unknown because we want to be able to pass any expression as a condition. For example, a
data
variable that might beundefined
.
Simple, isn't it?
Going back to our previous example, it looks like this:
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Layout>
<Show condition={isLoading}>
<p>Loading post...</p>
</Show>
<Show condition={error}>
<h2 style={{ color: 'red' }}>Error</h2>
<p>Unable to retrieve post.</p>
<pre>{error.message}</pre>
</Show>
<Show condition={data}>
<article>
<h1>{data.title}</h1>
<p>{data.content}</p>
</article>
</Show>
</Layout>
);
};
I find this much better!
... but there are two problems, one of which is major:
- Eager evaluation of children
- TypeScript doesn't narrow the type of the condition
Eager Evaluation of JSX
Let's revisit our previous example, simplified to highlight the part we're interested in.
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Show condition={data}>
<article>
<h1>{data.title}</h1>
<p>{data.content}</p>
</article>
</Show>
);
};
If the data
variable is initially undefined
, what do you think the behavior of the PostPage
component will be?
An error will be thrown: you cannot access the title
and content
properties of the data
variable when it's undefined
...
And yet, the condition passed to the Show
component should ensure that data
is defined before accessing it, right?
The problem is that the children of the Show
component are evaluated immediately, even before the component renders.
This seems logical: React components are functions that have parameters (props) and return React elements.
And as in JavaScript, the arguments of a function are evaluated before that function is called; the JSX passed as children
is evaluated even before entering the Show
component.
This is called eager evaluation.
But couldn't we defer the evaluation of our children
until we actually need them?
Lazy Evaluation
How can we evaluate our children
only when the condition is true?
What if we used a function? 🤔
Instead of directly receiving JSX as props, we could pass a function that returns that JSX. This would allow it to be executed only when our condition is true.
We then get the following code:
type Props = {
condition: unknown;
children: () => ReactNode;
};
export const Show: FC<Props> = ({ condition, children }) =>
condition ? children() : null;
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Show condition={data}>
{() => (
<article>
<h1>{data!.title}</h1>
<p>{data!.content}</p>
</article>
)}
</Show>
);
};
It works! 🎉
Deferring the evaluation of our variable until we actually need it is called lazy evaluation.
To accept both a function and JSX directly, we can rewrite our Show
component like this:
export const Show: FC<Props> = ({ condition, children }) => {
if (!condition) {
return null;
}
return typeof children === 'function' ?
children() : children;
};
TypeScript Doesn't Narrow the Type of the Condition
Let's go back to our example:
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Show condition={data}>
{() => (
<article>
<h1>{data!.title}</h1>
<p>{data!.content}</p>
</article>
)}
</Show>
);
};
You must have noticed the use of the Non-null assertion operator !
to access the properties of data
: data!.title
and data!.content
.
TypeScript doesn't perform type narrowing on the data
variable in this context. Even though data
is evaluated as "truthy" in the condition, its type remains unchanged inside the children
function. TypeScript continues to consider it as potentially undefined
. That's why one solution is to use the !
operator.
Enter Generics 😎
To have something more robust, we can use generic types.
Instead of using an unknown
type for our condition, we use a type T
that we re-inject as a parameter to our children
function by declaring it as non-null:
type Props<T> = {
condition: T;
children: ReactNode | ((value: NonNullable<T>) => ReactNode);
};
const Show = <T,>({ condition, children }: Props<T>) => {
if (!condition) {
return null;
}
return typeof children === 'function' ?
children(condition) : children;
};
const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Show condition={data}>
{(definedData) => (
<article>
<h1>{definedData.title}</h1>
<p>{definedData.content}</p>
</article>
)}
</Show>
);
};
The !
operator is no longer necessary, and we gain type safety!
Conclusion
Using the latest version of our Show
component and revisiting our first example, we get the following result:
export const PostPage = () => {
const { data, error, isLoading } = useQuery();
return (
<Layout>
<Show condition={isLoading}>
<p>Loading post...</p>
</Show>
<Show condition={error}>
{definedError => (
<>
<h2 style={{ color: 'red' }}>Error</h2>
<p>Unable to retrieve post.</p>
<pre>{definedError.message}</pre>
</>
)}
</Show>
<Show condition={data}>
{definedData => (
<article>
<h1>{definedData.title}</h1>
<p>{definedData.content}</p>
</article>
)}
</Show>
</Layout>
);
};
Now let's take a step back and recall our main objective: to improve readability.
Have we achieved our goal?
What do you think? 😉
How do you handle conditional rendering in your React components?
I'd be curious to discuss it with you.
Top comments (1)
I really like this pattern and use it regularly. You just need to read the JSX to clearly understand what's going on <3