React Integration
The @hex-di/guard-react package provides React components and hooks for integrating authorization into your UI, with full support for React Suspense.
Installation
npm install @hex-di/guard-react
SubjectProvider
The SubjectProvider component provides the current AuthSubject to all child components via React Context.
import { SubjectProvider } from "@hex-di/guard-react";
import { createAuthSubject } from "@hex-di/guard";
function App() {
const [subject, setSubject] = useState(null);
useEffect(() => {
// Fetch current user and create subject
fetchCurrentUser().then(user => {
setSubject(createAuthSubject({
id: user.id,
roles: user.roles,
permissions: user.permissions,
attributes: user.attributes,
}));
});
}, []);
return (
<SubjectProvider value={subject}>
{/* Your app components */}
</SubjectProvider>
);
}
Can/Cannot Components
Conditional rendering based on policy evaluation.
Can Component
Renders children only if the policy grants access.
import { Can } from "@hex-di/guard-react";
import { hasPermission } from "@hex-di/guard";
function UserList() {
return (
<Can policy={hasPermission(ReadUsers)}>
<div>
{/* This only renders if user has ReadUsers permission */}
<UserTable />
</div>
</Can>
);
}
// With fallback
function AdminPanel() {
return (
<Can
policy={hasRole(AdminRole)}
fallback={<div>Access denied - admin only</div>}
>
<AdminDashboard />
</Can>
);
}
Cannot Component
Renders children only if the policy denies access.
import { Cannot } from "@hex-di/guard-react";
import { hasAttribute } from "@hex-di/guard";
function TrialBanner() {
return (
<Cannot policy={hasAttribute("subscription", "premium")}>
<div className="banner">
Upgrade to Premium for more features!
</div>
</Cannot>
);
}
Hooks
All hooks come in two variants: standard (suspends) and deferred (never suspends).
useSubject() / useSubjectDeferred()
Access the current subject.
import { useSubject, useSubjectDeferred } from "@hex-di/guard-react";
// Suspends while subject is loading
function UserProfile() {
const subject = useSubject();
return <div>Welcome, {subject.id}!</div>;
}
// Never suspends - returns null while loading
function UserStatus() {
const subject = useSubjectDeferred();
if (!subject) {
return <div>Loading...</div>;
}
return <div>Status: {subject.attributes.status}</div>;
}
useCan() / useCanDeferred()
Boolean check for policy evaluation.
import { useCan, useCanDeferred } from "@hex-di/guard-react";
// Suspends while evaluating
function EditButton() {
const canEdit = useCan(hasPermission(WriteUsers));
return (
<button disabled={!canEdit}>
Edit
</button>
);
}
// Never suspends - returns loading state
function DeleteButton() {
const { allowed, loading } = useCanDeferred(hasPermission(DeleteUsers));
return (
<button disabled={loading || !allowed}>
{loading ? "..." : "Delete"}
</button>
);
}
usePolicy() / usePolicyDeferred()
Get the full Decision object with trace.
import { usePolicy, usePolicyDeferred } from "@hex-di/guard-react";
// Suspends while evaluating
function ResourceView({ resourceId }) {
const decision = usePolicy(
allOf(
hasPermission(ReadResource),
hasResourceAttribute("id", resourceId)
)
);
if (!decision.granted) {
return <div>Access denied: {decision.reason}</div>;
}
// Use decision.visibleFields to filter displayed data
return <ResourceDetails fields={decision.visibleFields} />;
}
// Never suspends
function ResourceStatus() {
const { decision, loading } = usePolicyDeferred(hasRole(ViewerRole));
if (loading) return <Spinner />;
if (!decision?.granted) return <AccessDenied />;
return <ResourceContent />;
}
usePolicies() / usePoliciesDeferred()
Evaluate multiple named policies at once.
import { usePolicies, usePoliciesDeferred } from "@hex-di/guard-react";
// Suspends while evaluating
function Dashboard() {
const decisions = usePolicies({
canViewUsers: hasPermission(ReadUsers),
canViewReports: hasPermission(ReadReports),
canManageSettings: hasRole(AdminRole),
});
return (
<div>
{decisions.canViewUsers.granted && <UserWidget />}
{decisions.canViewReports.granted && <ReportsWidget />}
{decisions.canManageSettings.granted && <SettingsWidget />}
</div>
);
}
// Never suspends
function Navigation() {
const { decisions, loading } = usePoliciesDeferred({
users: hasPermission(ReadUsers),
admin: hasRole(AdminRole),
});
if (loading) return <NavSkeleton />;
return (
<nav>
{decisions.users?.granted && <Link to="/users">Users</Link>}
{decisions.admin?.granted && <Link to="/admin">Admin</Link>}
</nav>
);
}
createGuardHooks() Factory
Create a typed set of hooks for your application's specific permissions and roles.
import { createGuardHooks } from "@hex-di/guard-react";
import { createPermission, createRole } from "@hex-di/guard";
// Define your app's permissions
const Permissions = {
ReadPosts: createPermission("ReadPosts"),
WritePosts: createPermission("WritePosts"),
DeletePosts: createPermission("DeletePosts"),
} as const;
// Define your app's roles
const Roles = {
Reader: createRole("Reader", {
permissions: [Permissions.ReadPosts],
}),
Author: createRole("Author", {
permissions: [Permissions.WritePosts],
inherits: [Roles.Reader],
}),
} as const;
// Create typed hooks
export const {
useSubject,
useSubjectDeferred,
useCan,
useCanDeferred,
usePolicy,
usePolicyDeferred,
usePolicies,
usePoliciesDeferred,
} = createGuardHooks<typeof Permissions, typeof Roles>();
// Now use throughout your app with full type safety
function BlogPost() {
const canEdit = useCan(hasPermission(Permissions.WritePosts));
// TypeScript knows about your specific permissions!
}
Suspense vs Deferred
When to use Suspense hooks (standard)
Use the standard hooks when:
- The component shouldn't render until authorization is determined
- You have a Suspense boundary with a loading fallback
- You want React's built-in loading states
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<SubjectProvider value={subject}>
<AuthorizedContent />
</SubjectProvider>
</Suspense>
);
}
function AuthorizedContent() {
// This suspends, so LoadingSpinner shows while loading
const canView = useCan(hasPermission(ViewContent));
if (!canView) return <AccessDenied />;
return <Content />;
}
When to use Deferred hooks
Use deferred hooks when:
- You need to show partial UI while authorization loads
- You want custom loading states
- You're not using Suspense boundaries
function PartialUI() {
const { allowed, loading } = useCanDeferred(hasPermission(EditContent));
return (
<div>
<h1>Content Title</h1>
<p>Content body...</p>
{loading ? (
<Skeleton />
) : allowed ? (
<EditButton />
) : null}
</div>
);
}
Types
SubjectState
type SubjectState = {
readonly subject: AuthSubject | null;
readonly loading: boolean;
readonly error: Error | null;
};
CanResult
type CanResult = {
readonly allowed: boolean;
readonly loading: boolean;
};
PolicyResult
type PolicyResult = {
readonly decision: Decision | null;
readonly loading: boolean;
};
Error Handling
MissingSubjectProviderError
Thrown when hooks are used outside of a SubjectProvider.
import { MissingSubjectProviderError } from "@hex-di/guard-react";
// This will throw MissingSubjectProviderError
function BadComponent() {
const subject = useSubject(); // Error! No provider
}
// Wrap with provider to fix
function GoodApp() {
return (
<SubjectProvider value={subject}>
<GoodComponent />
</SubjectProvider>
);
}
Complete Example
import React, { Suspense, useState, useEffect } from "react";
import {
SubjectProvider,
Can,
Cannot,
useCan,
usePolicy,
usePoliciesDeferred,
} from "@hex-di/guard-react";
import {
createAuthSubject,
hasPermission,
hasRole,
allOf,
} from "@hex-di/guard";
// Define your permissions and roles
const Permissions = {
ViewDashboard: createPermission("ViewDashboard"),
EditSettings: createPermission("EditSettings"),
};
const Roles = {
User: createRole("User", {
permissions: [Permissions.ViewDashboard],
}),
Admin: createRole("Admin", {
permissions: [Permissions.EditSettings],
inherits: [Roles.User],
}),
};
function App() {
const [subject, setSubject] = useState(null);
useEffect(() => {
// Simulate fetching user data
fetchUser().then(user => {
setSubject(createAuthSubject({
id: user.id,
roles: user.roles,
permissions: [],
attributes: user.attributes,
}));
});
}, []);
return (
<SubjectProvider value={subject}>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</SubjectProvider>
);
}
function Dashboard() {
const canView = useCan(hasPermission(Permissions.ViewDashboard));
if (!canView) {
return <div>You don't have access to the dashboard</div>;
}
return (
<div>
<h1>Dashboard</h1>
<Can policy={hasRole(Roles.Admin)}>
<AdminPanel />
</Can>
<Cannot policy={hasRole(Roles.Admin)}>
<div>Contact an admin for advanced features</div>
</Cannot>
<NavigationMenu />
</div>
);
}
function NavigationMenu() {
const { decisions, loading } = usePoliciesDeferred({
dashboard: hasPermission(Permissions.ViewDashboard),
settings: hasPermission(Permissions.EditSettings),
});
if (loading) {
return <div>Loading menu...</div>;
}
return (
<nav>
{decisions.dashboard?.granted && (
<a href="/dashboard">Dashboard</a>
)}
{decisions.settings?.granted && (
<a href="/settings">Settings</a>
)}
</nav>
);
}