AclProvider
Provide the current user's roles and permissions, and their loading state, to HasAccess and useAcl.
AclProvider holds the current user's roles and permissions and exposes an isAuthorized check to everything below it. HasAccess and useAcl both read from it, and both throw if no provider is rendered above them.
The provider does not fetch anything. You load the ACL data however you like (an API call, a session token, a server prop) and pass the result in, along with a loading flag.
Import
import { AclProvider } from "@piplup/react-acl";Usage
This example loads roles and permissions from a fake API. Switch users to see the loading state, then the content each user can see.
"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 { AclProvider, HasAccess } from "@piplup/react-acl";type Acl = { roles: string[]; permissions: string[] };// Stands in for a request to your auth API.const users: Record<string, Acl> = { guest: { roles: ["guest"], permissions: [] }, viewer: { roles: ["viewer"], permissions: ["report:read"] }, admin: { roles: ["admin"], permissions: ["report:read", "report:delete"] },};function fetchAcl(user: string): Promise<Acl> { return new Promise((resolve) => setTimeout(() => resolve(users[user]), 1200));}const emptyAcl: Acl = { roles: [], permissions: [] };export default function Page() { const [user, setUser] = React.useState("guest"); const [acl, setAcl] = React.useState<Acl>(emptyAcl); const [loading, setLoading] = React.useState(true); React.useEffect(() => { let ignore = false; fetchAcl(user).then((result) => { if (!ignore) { setAcl(result); setLoading(false); } }); return () => { ignore = true; }; }, [user]); return ( <Stack spacing={2}> <Stack direction="row" spacing={1}> {Object.keys(users).map((name) => ( <Button key={name} variant={user === name ? "contained" : "outlined"} onClick={() => { if (name !== user) { setLoading(true); setUser(name); } }} > Sign in as {name} </Button> ))} </Stack> <AclProvider loading={loading} roles={acl.roles} permissions={acl.permissions}> <HasAccess permissions={["report:read"]} loading={<CircularProgress size={24} />} fallback={<Alert severity="warning">You can't view reports.</Alert>} > <Typography>Quarterly report: 1,204 orders.</Typography> </HasAccess> <HasAccess permissions={["report:delete"]} loading={null}> <div> <Button color="error" variant="outlined"> Delete report </Button> </div> </HasAccess> </AclProvider> </Stack> );}Loading ACL data asynchronously
Set loading to true until your request resolves, then pass the real lists. While loading is true:
isAuthorizedreturnsfalsefor every check, whatever the lists contain.HasAccessrenders itsloadingprop. It does not renderfallback, so users don't see an "access denied" message flash before the data arrives.useAcl().loadingistrue, so your own components can show a placeholder.
Start with loading={true} on the first render, not false. With loading={false} and empty lists, every check that names a role or permission fails and HasAccess shows its fallback until the data arrives.
function AclRoot({ children }: { children: React.ReactNode }) {
const [acl, setAcl] = React.useState({ roles: [], permissions: [] });
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch("/api/me/acl")
.then((res) => res.json())
.then((data) => setAcl({ roles: data.roles, permissions: data.permissions }))
.finally(() => setLoading(false));
}, []);
return (
<AclProvider loading={loading} roles={acl.roles} permissions={acl.permissions}>
{children}
</AclProvider>
);
}If you already have the data when rendering, for example from the server or a decoded token, pass loading={false}.
How it works
Matching. isAuthorized compares the roles and permissions a check asks for against the lists you passed to the provider. Values are compared with ===, so the number 1 does not match the string "1". The rules for each check are covered on the HasAccess page.
Updates. When roles, permissions or loading change, the context value changes and every HasAccess and useAcl 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 roles or permissions array is a new reference. Inline literals such as roles={["admin"]} create a new array on every render, which re-renders all consumers each time the provider renders. Keep the arrays in state, or wrap them in useMemo.
Nesting. A nested AclProvider replaces the outer one for its subtree. Lists are not merged, so the inner provider must receive the full set of roles and permissions that should apply there.
Props
Prop
Type
roles and permissions accept string and number values, so you can use names like "admin" or numeric IDs from your backend.