Declarative permissions in React with <Can>
rbac-fs/react gives you two ways to check a permission: a declarative <Can> component for JSX, and an imperative usePermission() hook for everything else — routing guards, form validation logic, anywhere a boolean is more useful than a React element.
The provider
import { RBACClient } from 'rbac-fs/client';
import { RbacProvider } from 'rbac-fs/react';
const client = new RBACClient(await fetch('/me/permissions').then((r) => r.json()));
function App() {
return (
<RbacProvider client={client}>
{/* rest of your app */}
</RbacProvider>
);
}One RBACClient instance, fetched once from your snapshot endpoint, provided once at the root.
Declarative: <Can>
import { Can } from 'rbac-fs/react';
function ApproveButton() {
return (
<Can I="approve" a="invoice" fallback={<span>You can't approve invoices</span>}>
<button>Approve invoice</button>
</Can>
);
}The I/a prop naming (action / resource) intentionally mirrors CASL's convention rather than inventing a new one — if your team has used CASL before, this reads immediately; if not, it's a one-time thing to learn, documented once, applied everywhere <Can> shows up.
Condition context flows through props
function OwnExpenseReportButton({ ownerId }: { ownerId: string }) {
return (
<Can I="approve" a="expense-report" context={{ owner_id: ownerId }}>
<button>Approve my expense report</button>
</Can>
);
}The context prop is passed straight through to the underlying client.can() call, so a when: 'owner_id == user.id' conditional grant on the snapshot works exactly the same from <Can> as it does calling the client directly.
Imperative: usePermission()
function ImperativeCheck() {
const can = usePermission(); // same client.can(), for use outside JSX
return can('invoice', 'approve') ? <p>Allowed</p> : <p>Denied</p>;
}For a runnable, headless-rendered verification of both, see Tutorial: testing rbac-fs's React components without a browser.