AccessControlProvider
Provide the current principal — roles and permissions — and its loading state to Can, useAccessControl and everything below it.
AccessControlProvider holds the current principal and exposes it, plus checks bound to it, to
everything below. Can and
useAccessControl both read from it, and both throw if
no provider is rendered above them.
The provider does not fetch anything. You load the principal however you like — an API call, a session token, a server prop — and pass the result in.
Import
import { AccessControlProvider } from "@piplup/react-acl";Usage
This example loads the principal from a fake API. Switch users to see the loading state, then what
each one can reach. The admin holds report:*, so the delete button appears without
report:delete being granted explicitly.
"use client";import * as React from "react";import Alert from "@mui/material/Alert";import Button from "@mui/material/Button";import CircularProgress from "@mui/material/CircularProgress";import Stack from "@mui/material/Stack";import Typography from "@mui/material/Typography";import { AccessControlProvider, Can, type AccessPrincipal } from "@piplup/react-acl";// Stands in for a request to your auth API.const users: Record<string, AccessPrincipal> = { guest: { roles: ["guest"], permissions: [] }, viewer: { roles: ["viewer"], permissions: ["report:read"] }, admin: { roles: ["admin"], permissions: ["report:*"] },};function fetchPrincipal(user: string): Promise<AccessPrincipal> { return new Promise((resolve) => setTimeout(() => resolve(users[user]), 1200));}type Loaded = { user: string; principal: AccessPrincipal };export default function Page() { const [user, setUser] = React.useState("guest"); const [loaded, setLoaded] = React.useState<Loaded | null>(null); React.useEffect(() => { let ignore = false; fetchPrincipal(user).then((principal) => { if (!ignore) { setLoaded({ user, principal }); } }); return () => { ignore = true; }; }, [user]); // A principal loaded for a different user is stale, so it counts as not loaded // yet. `undefined` is exactly what the provider reads as "still loading", so no // separate loading flag is needed. const principal = loaded?.user === user ? loaded.principal : undefined; return ( <Stack spacing={2}> <Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}> {Object.keys(users).map((name) => ( <Button key={name} variant={user === name ? "contained" : "outlined"} onClick={() => setUser(name)} > Sign in as {name} </Button> ))} </Stack> <AccessControlProvider principal={principal}> <Can permission="report:read" loading={<CircularProgress size={24} />} fallback={<Alert severity="warning">You can't view reports.</Alert>} > <Typography>Quarterly report: 1,204 orders.</Typography> </Can> {/* admin holds `report:*`, which grants every report permission. */} <Can permission="report:delete"> <div> <Button color="error" variant="outlined"> Delete report </Button> </div> </Can> </AccessControlProvider> </Stack> );}Loading
A principal of undefined means "not fetched yet", so loading defaults to
principal === undefined and you can usually leave the prop out. While loading:
- every check returns
false, whatever the principal contains; Canrenders itsloadingprop. It does not renderfallback, so users don't see an "access denied" message flash before the data arrives;useAccessControl().loadingistrue, so your own components can show a placeholder.
function AclRoot({ children }: { children: React.ReactNode }) {
const [principal, setPrincipal] = React.useState<AccessPrincipal>(undefined);
React.useEffect(() => {
fetch("/api/me/acl")
.then((res) => res.json())
.then(setPrincipal);
}, []);
return <AccessControlProvider principal={principal}>{children}</AccessControlProvider>;
}Pass loading explicitly in the two cases the default cannot read:
- you already hold a principal and are refreshing it, so it is not
undefinedbut the checks should still be suspended; undefinedis a legitimate loaded value in your code, and you track loading separately.
null is a loaded principal with no access — an anonymous visitor. It is denied, not loading.
How it works
Matching. Permissions are matched by segment with wildcard support; roles are matched exactly. See Permissions.
Updates. When principal or loading changes, the context value changes and every Can and
useAccessControl consumer re-renders with the new result. You don't need to remount anything after
a login, logout or role change.
The context value is rebuilt whenever the principal object is a new reference. An inline literal
such as principal={{ roles: ["admin"] }} creates a new object on every render, which re-renders
every consumer each time the provider renders. Keep the principal in state, or wrap it in
useMemo.
Nesting. A nested provider replaces the outer one for its subtree. Principals are not merged, so the inner provider must receive the full set of roles and permissions that should apply there.
Props
Prop
Type