Skip to main content

Guards

An easy-to-use pattern for UI driven content guards.

It can be used for feature flags, role limitations, etc...

Component

/src/**/guards/Guard.tsx
import React, { PropsWithChildren } from 'react';

function isNil(value: unknown | null | undefined): value is null | undefined {
return value === null || value === undefined;
}

function getArray<T>(value?: T | T[] | null): T[] {
if (isNil(value)) {
return [];
}

return Array.isArray(value) ? value : [value];
}

type GuardProps = { allowed?: boolean; loading?: boolean; };

export const Allowed = ({ children }: PropsWithChildren<unknown>) => <>{children}</>;
export const Fallback = ({ children }: PropsWithChildren<unknown>) => <>{children}</>;
export const Pending = ({ children }: PropsWithChildren<unknown>) => <>{children}</>;

export function Guard({ allowed, pending, children }: PropsWithChildren<GuardProps>) {
const childrenArray = getArray(children);

const allowedChildren = childrenArray.filter((child) => child.type?.name === Allowed.name);
const fallbackChildren = childrenArray.filter((child) => child.type?.name === Fallback.name);
const pendingChildren = childrenArray.filter((child) => child.type?.name === Pending.name);

if ((fallbackChildren?.length || pendingChildren?.length) && !allowedChildren?.length) {
throw new Error('You cannot use <Fallback /> or <Pending /> without <Allowed />');
}

if (pending) {
if (pendingChildren?.length) {
return <>{pendingChildren}</>;
}

return null;
}

if (!allowed) {
if (fallbackChildren?.length) {
return <>{fallbackChildren}</>;
}

return null;
}

if (allowedChildren.length) {
return <>{allowedChildren}</>;
}

return <>{children}</>;
}

Usage

Simple

/src/**/guards/WithRole.tsx
import React, { PropsWithChildren } from 'react';

import { Guard } from './Guard.tsx';

type WithRoleProps = { role: 'admin' | 'user' };

export function useWithRole({ role }: WithRoleProps) {
// Implement role check logic here

return { allowed, loading };
}

export function WithRole({ children, ...props }: PropsWithChildren<WithRoleProps>) {
const { allowed, loading } = useWithRole(props);

return <Guard allowed={allowed} pending={loading} children={children} />
}
/src/App.tsx
import React, { PropsWithChildren } from 'react';

import { WithRole } from './**/guards/WithRole';

export function App() {
return (
<main>
<h1>My Website</h1>
<WithRole role="admin">
<p>Secret content only for admins</p>
</WithRole>
</main>
)
}

With Fallback and Pending content

/src/**/guards/WithFeatureFlag.tsx
import React, { PropsWithChildren } from 'react';

import { Guard } from './Guard.tsx';

type WithFeatureFlagProps = { feature: 'future-home' };

export function useWithFeatureFlag({ role }: WithFeatureFlagProps) {
// Implement feature flag logic here

return { allowed, loading };
}

export function WithFeatureFlag({ children, ...props }: PropsWithChildren<WithFeatureFlagProps>) {
const { allowed, loading } = useWithRole(props);

return <Guard allowed={allowed} pending={loading} children={children}/>;
}
/src/App.tsx
import React, { PropsWithChildren } from 'react';

import { FutureHome, Home } from './**/home';
import { WithFeatureFlag } from './**/guards/WithFeatureFlag';

export function App() {
return (
<main>
<h1>My Website</h1>
<WithFeatureFlag feature="future-home">
<Allowed>
<FutureHome />
</Allowed>
<Fallback>
<Home />
</Fallback>
<Pending>
<p>Loading...</p>
</Pending>
</WithFeatureFlag>
</main>
)
}