The Pitfalls of Using <Guard> or <If> Components in React
Use Logical AND Operator (&&
) or Babel Plugin Instead
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:
- https://github.com/mui/material-ui/blob/fe5c2c9f2d27426d2551bce213c45e4c17f6442e/packages/mui-lab/src/TabList/TabList.js#L12
- https://github.com/mui/material-ui/blob/fe5c2c9f2d27426d2551bce213c45e4c17f6442e/packages/mui-material/src/Tabs/Tabs.js#L669
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.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: