Skip to content
View as Markdown

WithGuard ​

WithGuard is a component for conditionally rendering UI elements based on guard evaluation. It uses the same guard system as route-level guards, making permission logic reusable throughout your application.

Live preview in the UI Catalogue →

Import ​

tsx
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";

Basic Usage ​

tsx
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";

const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());

function AdminPanel() {
  return (
    <div>
      <h1>Dashboard</h1>

      <WithGuard guards={[isAdmin]}>
        <Button>Delete All Users</Button>
      </WithGuard>
    </div>
  );
}

If the user is not an admin, the button won't render.

Props ​

PropTypeDefaultDescription
guardsGuard[]RequiredArray of guard functions to evaluate
childrenReact.ReactNodeRequiredContent to render when all guards pass
fallbackReact.ReactNodenullContent to render when any guard returns hidden()
loadingReact.ReactNodenullContent to show while async guards are evaluating

Guard Functions ​

WithGuard uses the same Guard type as route guards:

typescript
type Guard = (ctx: GuardContext) => GuardResult | Promise<GuardResult>;

type GuardResult =
  | { type: "pass" } // Allow rendering
  | { type: "hidden" }; // Hide content (render fallback)

Note: Unlike route guards, redirectTo() is not supported in WithGuard. Use hidden() with a fallback that handles navigation if needed.

Creating Guards ​

tsx
import { type Guard, pass, hidden } from "@tailor-platform/app-shell";

// Simple guard
const requireAuth: Guard = ({ context }) => {
  return context.currentUser ? pass() : hidden();
};

// Async guard with API call
const hasPermission: Guard = async ({ context }) => {
  const canAccess = await fetch("/api/permissions/admin")
    .then((r) => r.json())
    .then((data) => data.allowed);

  return canAccess ? pass() : hidden();
};

// Parameterized guard (curried function)
const isOwner =
  (resourceId: string): Guard =>
  ({ context }) => {
    return context.currentUser?.id === resourceId ? pass() : hidden();
  };

Examples ​

Simple Role Check ​

tsx
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());

<WithGuard guards={[isAdmin]}>
  <Button>Admin Settings</Button>
</WithGuard>;

With Fallback ​

Show alternative content when guard fails:

tsx
<WithGuard guards={[isAdmin]} fallback={<p>You need admin access to view this.</p>}>
  <AdminPanel />
</WithGuard>

With Loading State ​

Show loading spinner while checking permissions:

tsx
const checkPermission: Guard = async ({ context }) => {
  const allowed = await api.checkPermission("admin");
  return allowed ? pass() : hidden();
};

<WithGuard guards={[checkPermission]} loading={<Spinner />} fallback={<UpgradePrompt />}>
  <PremiumFeature />
</WithGuard>;

Multiple Guards ​

All guards must pass for content to render:

tsx
const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());

const hasFeatureFlag = ({ context }) => (context.featureFlags?.betaFeatures ? pass() : hidden());

<WithGuard guards={[isAdmin, hasFeatureFlag]}>
  <BetaAdminFeature />
</WithGuard>;

Parameterized Guards ​

Use curried functions to create reusable parameterized guards:

tsx
const isOwner =
  (resourceId: string): Guard =>
  ({ context }) => {
    return context.currentUser?.id === resourceId ? pass() : hidden();
  };

function ResourceActions({ resourceId }) {
  return (
    <WithGuard guards={[isOwner(resourceId)]}>
      <Button>Edit</Button>
      <Button>Delete</Button>
    </WithGuard>
  );
}

In Sidebar Navigation ​

Conditionally show navigation items:

tsx
import { DefaultSidebar, SidebarItem, WithGuard } from "@tailor-platform/app-shell";

const isAdmin = ({ context }) => (context.currentUser?.role === "admin" ? pass() : hidden());

<DefaultSidebar>
  <SidebarItem to="/dashboard" />
  <SidebarItem to="/products" />

  <WithGuard guards={[isAdmin]}>
    <SidebarItem to="/admin" />
    <SidebarItem to="/settings" />
  </WithGuard>
</DefaultSidebar>;

Feature Flags ​

Toggle features based on flags:

tsx
const hasBetaFeature = ({ context }) => (context.featureFlags?.newDashboard ? pass() : hidden());

<WithGuard guards={[hasBetaFeature]} fallback={<OldDashboard />}>
  <NewDashboard />
</WithGuard>;

Subscription Tiers ​

Show features based on user's subscription:

tsx
const isPremium = ({ context }) =>
  context.currentUser?.subscription === "premium" ? pass() : hidden();

<WithGuard
  guards={[isPremium]}
  fallback={
    <Card>
      <h3>Premium Feature</h3>
      <p>Upgrade to access advanced analytics.</p>
      <Button>Upgrade Now</Button>
    </Card>
  }
>
  <AdvancedAnalytics />
</WithGuard>;

Dynamic Permissions ​

Check permissions from API:

tsx
const canDeleteOrder =
  (orderId: string): Guard =>
  async ({ context }) => {
    const response = await fetch(`/api/orders/${orderId}/permissions`);
    const { canDelete } = await response.json();
    return canDelete ? pass() : hidden();
  };

function OrderActions({ orderId }) {
  return (
    <div>
      <Button>View</Button>
      <Button>Edit</Button>
      <WithGuard guards={[canDeleteOrder(orderId)]} loading={<Spinner size="sm" />}>
        <Button variant="destructive">Delete</Button>
      </WithGuard>
    </div>
  );
}

Combining with Route Guards ​

Reuse the same guards for routes and UI:

tsx
// guards.ts
export const requireAuth: Guard = ({ context }) => {
  return context.currentUser ? pass() : hidden();
};

export const isAdmin: Guard = ({ context }) => {
  return context.currentUser?.role === "admin" ? pass() : hidden();
};

// routes.tsx
const adminModule = defineModule({
  path: "admin",
  component: AdminPage,
  guards: [requireAuth, isAdmin], // Route-level
});

// component.tsx
<WithGuard guards={[requireAuth, isAdmin]}>
  {" "}
  {/* UI-level */}
  <AdminSettings />
</WithGuard>;

Guard Evaluation ​

Guards are evaluated in order and stop at the first non-pass result:

tsx
const guards = [guardA, guardB, guardC];

// If guardA returns hidden(), guardB and guardC are NOT evaluated
<WithGuard guards={guards}>
  <Content />
</WithGuard>;

Performance ​

Caching ​

WithGuard caches guard results based on contextData reference:

  • Guards re-evaluate when contextData changes
  • Guards do NOT re-evaluate when props change (except guards array)

Suspense Integration ​

Async guards use React Suspense:

  • Shows loading prop while evaluating
  • Renders children or fallback when complete
  • Automatically suspends during evaluation

Context Data ​

Guards receive the same context as route guards:

typescript
// Define context type
declare module "@tailor-platform/app-shell" {
  interface AppShellRegister {
    contextData: {
      currentUser: User | null;
      featureFlags: FeatureFlags;
      permissions: string[];
    };
  }
}

// Use in guards
const hasPermission =
  (perm: string): Guard =>
  ({ context }) => {
    return context.permissions.includes(perm) ? pass() : hidden();
  };

Comparison: WithGuard vs Route Guards ​

FeatureWithGuardRoute Guards
PurposeConditional UI renderingRoute access control
LocationInside componentsModule/resource definitions
Supports pass()✅ Yes✅ Yes
Supports hidden()✅ Yes✅ Yes
Supports redirectTo()❌ No✅ Yes
Async guards✅ Yes✅ Yes
Reusable✅ Yes✅ Yes

Best Practices ​

Do: ​

  • ✅ Reuse guards between routes and UI
  • ✅ Use descriptive guard names (isAdmin, canEdit)
  • ✅ Provide fallback for better UX
  • ✅ Show loading state for async guards
  • ✅ Cache expensive permission checks

Don't: ​

  • ❌ Use redirectTo() in WithGuard (not supported)
  • ❌ Make guards too complex (extract to functions)
  • ❌ Forget to handle loading states
  • ❌ Skip fallback when hiding important features
  • ❌ Duplicate permission logic (reuse guards)

Accessibility ​

  • Content is fully removed from DOM when hidden (not just visually)
  • Screen readers won't announce hidden content
  • Loading states should be announced to screen readers
  • Fallback content should be accessible

TypeScript ​

Full type safety with TypeScript:

typescript
import { type Guard, type WithGuardProps } from "@tailor-platform/app-shell";

// Type-safe guard
const isAdmin: Guard = ({ context }) => {
  // context is fully typed
  return context.currentUser?.role === "admin" ? pass() : hidden();
};

// Type-safe props
const guardProps: WithGuardProps = {
  guards: [isAdmin],
  children: <AdminPanel />,
  fallback: <AccessDenied />,
};

Troubleshooting ​

Guard not re-evaluating ​

Problem: Guard doesn't update when data changes

Solution: Ensure contextData reference changes:

tsx
// ❌ Bad - mutating object
context.currentUser.role = "admin";

// ✅ Good - new reference
setContextData({
  ...contextData,
  currentUser: { ...currentUser, role: "admin" },
});

Infinite loading ​

Problem: Loading state never resolves

Solution: Ensure async guards return a result:

tsx
// ❌ Bad - no return
const checkPermission: Guard = async ({ context }) => {
  await fetch("/api/check");
  // Missing return!
};

// ✅ Good - returns result
const checkPermission: Guard = async ({ context }) => {
  const allowed = await fetch("/api/check").then((r) => r.json());
  return allowed ? pass() : hidden();
};

API Reference ​