The WithGuard component provides conditional rendering based on the same guard logic used in route definitions. Use it to control visibility of UI elements like sidebar items, buttons, or entire sections based on user permissions, feature flags, or other criteria.
Import
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
Basic Usage
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
const isAdmin = ({ context }) =>
context.currentUser?.role === "admin" ? pass() : hidden();
<WithGuard guards={[isAdmin]}>
<AdminPanel />
</WithGuard>
Props
Array of guard functions to evaluate. All guards must return pass() for children to render.Guards are evaluated in order, and evaluation stops at the first non-pass result.See Route Guards for guard function reference.
Content to render when all guards pass
fallback
React.ReactNode
default:"null"
Content to render when any guard returns hidden()<WithGuard
guards={[requirePremium]}
fallback={<UpgradePrompt />}
>
<PremiumFeature />
</WithGuard>
loading
React.ReactNode
default:"null"
Content to render while async guards are being evaluated<WithGuard
guards={[checkSubscriptionStatus]}
loading={<Spinner />}
>
<SubscriptionContent />
</WithGuard>
Guard Functions
WithGuard uses the same guard types as route guards. See the Route Guards documentation for complete details.
Guard Type
type Guard = (context: GuardContext) => GuardResult | Promise<GuardResult>;
type GuardContext = {
context: ContextData; // Custom context data from AppShell
};
type GuardResult =
| { type: "pass" } // Allow rendering
| { type: "hidden" }; // Deny rendering (show fallback)
Note: Unlike route guards, redirectTo() is not supported in WithGuard. Use hidden() with a fallback that handles navigation if needed.
Helper Functions
import { pass, hidden } from "@tailor-platform/app-shell";
// Allow rendering
const allowAll = () => pass();
// Deny rendering
const denyAll = () => hidden();
Common Use Cases
Role-Based Access Control
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
const isAdmin = ({ context }) =>
context.currentUser?.role === "admin" ? pass() : hidden();
const isManager = ({ context }) =>
context.currentUser?.role === "manager" ? pass() : hidden();
// Admin-only button
<WithGuard guards={[isAdmin]}>
<Button onClick={deleteAllData}>Delete All</Button>
</WithGuard>
// Manager or admin
<WithGuard guards={[isManager]}>
<ExportButton />
</WithGuard>
Feature Flags
const hasFeature = (featureName: string) => ({ context }) =>
context.featureFlags?.[featureName] ? pass() : hidden();
<WithGuard guards={[hasFeature("newDashboard")]}>
<NewDashboard />
</WithGuard>
// With fallback to old version
<WithGuard
guards={[hasFeature("newDashboard")]}
fallback={<OldDashboard />}
>
<NewDashboard />
</WithGuard>
Control visibility of navigation items:
import { DefaultSidebar, SidebarItem, WithGuard } from "@tailor-platform/app-shell";
const isAdmin = ({ context }) =>
context.currentUser?.role === "admin" ? pass() : hidden();
const CustomSidebar = () => (
<DefaultSidebar>
<SidebarItem to="/dashboard" />
<SidebarItem to="/orders" />
{/* Admin-only navigation */}
<WithGuard guards={[isAdmin]}>
<SidebarItem to="/admin" />
<SidebarItem to="/settings" />
</WithGuard>
</DefaultSidebar>
);
Subscription-Based Access
const hasPremium = ({ context }) =>
context.subscription?.tier === "premium" ? pass() : hidden();
<WithGuard
guards={[hasPremium]}
fallback={
<Card>
<CardHeader>
<CardTitle>Premium Feature</CardTitle>
</CardHeader>
<CardContent>
<p>Upgrade to access advanced analytics</p>
<Button>Upgrade Now</Button>
</CardContent>
</Card>
}
>
<AdvancedAnalytics />
</WithGuard>
Multiple Guards
All guards must pass for children to render:
const isAuthenticated = ({ context }) =>
context.currentUser ? pass() : hidden();
const hasPermission = (permission: string) => ({ context }) =>
context.currentUser?.permissions.includes(permission) ? pass() : hidden();
const isActive = ({ context }) =>
context.currentUser?.status === "active" ? pass() : hidden();
// Must be authenticated AND have permission AND be active
<WithGuard guards={[isAuthenticated, hasPermission("delete"), isActive]}>
<DeleteButton />
</WithGuard>
Parameterized Guards
Create reusable guard factories:
const hasRole = (role: string) => ({ context }) =>
context.currentUser?.role === role ? pass() : hidden();
const hasAnyRole = (...roles: string[]) => ({ context }) =>
roles.includes(context.currentUser?.role) ? pass() : hidden();
const ownsResource = (resourceId: string) => ({ context }) =>
context.currentUser?.id === resourceId ? pass() : hidden();
// Usage
<WithGuard guards={[hasRole("admin")]}>
<AdminTools />
</WithGuard>
<WithGuard guards={[hasAnyRole("admin", "manager")]}>
<ManagementPanel />
</WithGuard>
const { id } = useParams();
<WithGuard guards={[ownsResource(id)]}>
<EditButton />
</WithGuard>
Async Guards
WithGuard supports async guards with Suspense integration:
const checkSubscription = async ({ context }) => {
const subscription = await context.apiClient.getSubscription();
return subscription.active ? pass() : hidden();
};
<WithGuard
guards={[checkSubscription]}
loading={<Spinner />}
fallback={<SubscriptionExpired />}
>
<PremiumContent />
</WithGuard>
Note: The guard result is cached based on contextData reference equality. Guards re-evaluate only when contextData changes.
Context Data Access
Guards receive the context data passed to AppShell:
// 1. Define context type
declare module "@tailor-platform/app-shell" {
interface AppShellRegister {
contextData: {
currentUser: User | null;
apiClient: ApiClient;
featureFlags: Record<string, boolean>;
};
}
}
// 2. Pass context to AppShell
<AppShell
modules={modules}
contextData={{
currentUser,
apiClient,
featureFlags,
}}
>
<SidebarLayout />
</AppShell>
// 3. Access in guards (fully typed)
const myGuard = ({ context }) => {
// context.currentUser is typed
// context.apiClient is typed
// context.featureFlags is typed
return context.currentUser ? pass() : hidden();
};
Comparison with Route Guards
| Feature | Route Guards | WithGuard |
|---|
| Purpose | Control route access | Control component visibility |
pass() | Allow route access | Render children |
hidden() | Show 404 page | Render fallback |
redirectTo() | Navigate to path | ❌ Not supported |
| Async support | ✅ Yes | ✅ Yes with Suspense |
| Context access | ✅ Full context | ✅ Full context |
Best Practices
1. Reuse Route Guards
Define guards once and use them in both routes and UI:
// guards.ts
export const isAdmin = ({ context }) =>
context.currentUser?.role === "admin" ? pass() : hidden();
// In route definition
const adminModule = defineModule({
path: "admin",
guards: [isAdmin],
// ...
});
// In component
<WithGuard guards={[isAdmin]}>
<AdminPanel />
</WithGuard>
2. Avoid Redirects in WithGuard
Don’t use redirectTo() in WithGuard guards. Handle navigation in the fallback:
// ❌ Bad - redirectTo not supported
<WithGuard guards={[() => redirectTo("/login")]}>
<Content />
</WithGuard>
// ✅ Good - navigate in fallback
const LoginPrompt = () => {
const navigate = useNavigate();
return (
<Card>
<Button onClick={() => navigate("/login")}>Login</Button>
</Card>
);
};
<WithGuard guards={[isAuthenticated]} fallback={<LoginPrompt />}>
<Content />
</WithGuard>
3. Provide Meaningful Fallbacks
Show helpful messages instead of hiding content silently:
// ❌ Less helpful
<WithGuard guards={[hasPremium]}>
<AdvancedFeature />
</WithGuard>
// ✅ Better UX
<WithGuard
guards={[hasPremium]}
fallback={
<div className="text-muted-foreground">
This feature requires a premium subscription.
</div>
}
>
<AdvancedFeature />
</WithGuard>
4. Handle Loading States
Provide loading indicators for async guards:
<WithGuard
guards={[checkPermissions]}
loading={<Skeleton className="h-20" />}
>
<ProtectedContent />
</WithGuard>
5. Combine with Error Boundaries
Wrap WithGuard in error boundaries for async guard failures:
<ErrorBoundary fallback={<ErrorMessage />}>
<WithGuard guards={[asyncGuard]}>
<Content />
</WithGuard>
</ErrorBoundary>
TypeScript
import { type Guard, type GuardContext, pass, hidden } from "@tailor-platform/app-shell";
// Type-safe guard
const myGuard: Guard = ({ context }) => {
return context.currentUser ? pass() : hidden();
};
// Guard factory with types
const hasPermission = (permission: string): Guard => ({ context }) => {
return context.permissions?.includes(permission) ? pass() : hidden();
};