The Pitfalls of Using <Guard> or <If> Components in React

Use Logical AND Operator (&&) or Babel Plugin Instead

Thor Chen
Bits and Pieces
Published in
7 min readApr 12, 2023

--

Image generated using Midjourney

Background

Like many React developers, I prefer concise JSX syntax. When it comes to conditional rendering, many of us may wish to have a component that expresses control flow in JSX.

That is to say, compared to the syntax below:

<div>
{condition ? (
<div>Something</div>
) : (
<div>Something else</div>
)}
</div>

A more JSX-flavoured syntax may look better:

<div>
<Guard condition={condition} fallback={<div>Something else</div>}>
<div>Something</div>
</Guard>
</div>

By the way, this is essentially the same syntax used when working with the built-in <Suspense> component.

Furthermore, I prefer to keep the code as readable as possible, which means I am in favour of a flat control flow where the condition and result are collocated:

<div>
<Guard condition={condition}>
<div>Something</div>
</Guard>
<Guard condition={!condition}>
<div>Something else</div>
</Guard>
</div>

It seems I am not the only one who has had this idea. There are other libraries taking a similar approach such as react-if.

The advantages of having a concise JSX syntax become more obvious if we have multiple components to render in each branch:

// The ternary approach
<div>
{isLoggedIn ? (
<>
<WelcomeMessage />
<Dashboard />
<LogoutButton />
</>
) : (
<>
<LoginPrompt />
<RegisterButton />
</>
)}
</div>
// The Guard approach
<div>
<Guard condition={isLoggedIn}>
<WelcomeMessage />
<Dashboard />
<LogoutButton />
</Guard>

<Guard condition={!isLoggedIn}>
<LoginPrompt />
<RegisterButton />
</Guard>
</div>

If you are not convinced yet, let’s see a more complex example with multiple layers of conditions nested:

<div>
{isLoggedIn ? (
userRole === "admin" ? (
<>
<AdminDashboard />
<ManageUsers />
<LogoutButton />
</>
) : userRole === "editor" ? (
<>
<EditorDashboard />
<EditArticles />
<LogoutButton />
</>
) : (
<>
<UserDashboard />
<ViewArticles />
<LogoutButton />
</>
)
) : (
<>
<LoginPrompt />
<RegisterButton />
</>
)}
</div>

Can you easily tell what’s going on in the code above? I doubt that.

Let’s see a version of using <Guard> component instead:

<div>
<Guard condition={isLoggedIn && userRole === "admin"}>
<AdminDashboard />
<ManageUsers />
<LogoutButton />
</Guard>

<Guard condition={isLoggedIn && userRole === "editor"}>
<EditorDashboard />
<EditArticles />
<LogoutButton />
</Guard>

<Guard condition={isLoggedIn && userRole !== "admin" && userRole !== "editor"}>
<UserDashboard />
<ViewArticles />
<LogoutButton />
</Guard>

<Guard condition={!isLoggedIn}>
<LoginPrompt />
<RegisterButton />
</Guard>
</div>

Isn’t it much easier to understand? I believe you get it.

Problems

While I believe the <Guard> or <If> component has a more concise syntax, it does have some issues.

Firstly, it does not prevent React from evaluating the children component tree.

Let’s consider a simple example of the <Guard> component. Assuming that the <Guard> component is written as follows:

type Props = { condition: any; children: any };

const Guard: React.FC<Props> = ({ condition, children }) => {
if (Boolean(condition)) {
return children;
}

return null;
};

… and we use it in the following way:

const App: React.FC = () => {
const [str, setStr] = React.useState<string | null>("Something");

return (
<div>
<Guard condition={str}>
<p>{`Upper case: ${str!.toUpperCase()}`}</p>
</Guard>

<button onClick={() => setStr(null)}>Set null</button>
</div>
);
};

Please pay extra attention to this part:

<Guard condition={str}>
<p>{`Upper case: ${str!.toUpperCase()}`}</p>
</Guard>

We may think that the variable str is already guarded, and therefore we can safely call string functions inside the <Guard> component. Unfortunately, this is not the case. Clicking the button to set str to null results in a crash:

TypeError
Cannot read properties of null (reading 'toUpperCase')

We can try this on codesandbox:

Secondly, some UI libraries require specific child component types within a parent component.

For example, MUI requires us to put a <Tab> as a direct child within <Tabs> or <TabList>.

Imagine the following App component:

const App: React.FC = () => {
const [value, setValue] = React.useState("1");
const [extraTab, setExtraTab] = React.useState(false);

const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};

return (
<Box sx={{ width: "100%", typography: "body1" }}>
<button onClick={() => setExtraTab((prev) => !prev)}>
Toggle extra tab
</button>
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList onChange={handleChange} aria-label="lab API tabs example">
<Tab label="Item One" value="1" />
<Tab label="Item Two" value="2" />
<Tab label="Item Three" value="3" />
<Guard condition={extraTab}>
<Tab label="Item Four" value="4" />
</Guard>
{extraTab && <Tab label="Item Five" value="5" />}
</TabList>
</Box>
<TabPanel value="1">Item One</TabPanel>
<TabPanel value="2">Item Two</TabPanel>
<TabPanel value="3">Item Three</TabPanel>
<TabPanel value="4">Item Four</TabPanel>
<TabPanel value="5">Item Five</TabPanel>
</TabContext>
</Box>
);
};

The screenshot below shows the UI before clicking the toggle button:

…and the screenshot below shows the new two <Tab> components after the toggle:

However, clicking the fourth tab will not work — the <Tab> will not be highlighted, and the corresponding <TabPanel> will not appear.

This is because MUI assumes that the child elements within <TabList> and <Tabs> have specific props, see:

We can try this with codesandbox:

Solutions

Option #1: Stop using control-flow components, use && instead

By simply replacing the <Guard> component with &&, both issues can be solved.

In the first case, after the replacement, the frontend will no longer crash:

{str && <p>{`Upper case: ${str!.toUpperCase()}`}</p>}

In the second case, after the replacement, the <Tab> will work correctly (see the "Item Five" example in the code):

{extraTab && <Tab label="Item Five" value="5" />}

Furthermore, an example of using && to replace <Guard> in the “Background” section:

<div>
{isLoggedIn && userRole === "admin" && (
<>
<AdminDashboard />
<ManageUsers />
<LogoutButton />
</>
)}

{isLoggedIn && userRole === "editor" && (
<>
<EditorDashboard />
<EditArticles />
<LogoutButton />
</>
)}

{isLoggedIn && userRole !== "admin" && userRole !== "editor" && (
<>
<UserDashboard />
<ViewArticles />
<LogoutButton />
</>
)}

{!isLoggedIn && (
<>
<LoginPrompt />
<RegisterButton />
</>
)}
</div>

Not too bad, isn’t it :)

Please note that I am still strongly against using the ternary operator ?: in JSX, because it is really harmful on code readability and maintainability.

Option #2: Use a plugin to replace control-flow components during compilation

I have found a very interesting Babel plugin that allows us to write JSX with control-flow components but transform them into plain JavaScript during compilation. This means that the control-flow components will become “syntax sugar” in this case.

This library’s document says it will do the transformation during compilation as such:

// Before transformation
<If condition={test}>
<span>Truth</span>
</If>;

// After transformation
{
test ? <span>Truth</span> : null;
}

I have not tried it yet, but here is the package if you’re interested: babel-plugin-jsx-control-statements.

Conclusion

In conclusion, while control-flow components like <Guard> or <If> can make JSX syntax more concise, they come with some issues.

One of these issues is that they do not prevent React from evaluating the children component tree. Another issue is that some UI libraries require specific child component types within a parent component.

However, we can solve these issues by using alternative solutions. For example, we can replace control-flow components with && or use a Babel plugin to transform control-flow components into plain JavaScript during compilation.

Build React Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

Passionate JavaScript/TypeScript Developer with a Full-stack Background