useAccessControl
Read the principal and its loading state, and run permission and role checks in code with can and hasRole.
useAccessControl returns the current principal, its loading state, and two checks bound to it,
from the nearest AccessControlProvider.
Can is built on it.
Use Can when you show or hide a piece of markup. Use this hook when you need the answer as a
value: filtering a list of actions, disabling instead of hiding, choosing a redirect, or checking
access inside an event handler.
Import
import { useAccessControl } from "@piplup/react-acl";Usage
The toolbar keeps only the actions the current user may run, and the Export button checks their role
when it is clicked. The owner holds post:*, so every action shows without each one being granted.
"use client";import * as React from "react";import Button from "@mui/material/Button";import MenuItem from "@mui/material/MenuItem";import Stack from "@mui/material/Stack";import TextField from "@mui/material/TextField";import Typography from "@mui/material/Typography";import { AccessControlProvider, useAccessControl, type AccessPrincipal } from "@piplup/react-acl";const principals: Record<string, AccessPrincipal> = { guest: { roles: ["guest"], permissions: [] }, editor: { roles: ["editor"], permissions: ["post:create", "post:publish"] }, owner: { roles: ["owner"], permissions: ["post:*"] },};type User = keyof typeof principals;const actions = [ { label: "New post", permission: "post:create" }, { label: "Publish", permission: "post:publish" }, { label: "Delete", permission: "post:delete" },];function PostToolbar() { const { can, hasRole, loading } = useAccessControl(); // `can` is bound to the principal, so no need to pass it around. const visible = actions.filter((action) => can(action.permission)); // Check a role in an event handler, not only while rendering. const handleExport = () => { alert(hasRole(["editor", "owner"]) ? "Exporting posts" : "Sign in to export"); }; if (loading) { return <Typography>Checking access...</Typography>; } return ( <Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}> {visible.map((action) => ( <Button key={action.label} variant="contained"> {action.label} </Button> ))} <Button variant="outlined" onClick={handleExport}> Export </Button> </Stack> );}export default function Page() { const [user, setUser] = React.useState<User>("editor"); return ( <Stack spacing={2}> <TextField select size="small" label="Current user" value={user} onChange={(e) => setUser(e.target.value as User)} sx={{ maxWidth: 200 }} > {Object.keys(principals).map((name) => ( <MenuItem key={name} value={name}> {name} </MenuItem> ))} </TextField> {/* owner holds `post:*`, so every action shows without listing each one. */} <AccessControlProvider principal={principals[user]}> <PostToolbar /> </AccessControlProvider> </Stack> );}What it returns
type UseAccessControl = () => {
can: (
permission: Permission | readonly Permission[],
matchMode?: "ALL" | "ANY",
) => boolean;
hasRole: (role: Role | readonly Role[], matchMode?: "ALL" | "ANY") => boolean;
loading: boolean;
principal: AccessPrincipal;
};can and hasRole are the principal already applied, so you don't pass it in. They follow the same
rules as Can: an omitted or empty requirement passes, can defaults to ALL and hasRole
defaults to ANY. See how access is decided.
Both return false while loading is true, so guard on loading before treating a false as a
denial:
const { can, loading } = useAccessControl();
if (loading) return <Skeleton />;
return can("invoice:write") ? <EditInvoice /> : <ReadOnlyNotice />;Without that guard the read-only notice shows for a moment on first render, before the principal arrives.
principal is the object as given to the provider. Reach for it when you need to hand the principal
to something that is not a React component, such as a navigation filter or a client-side loader, using
the check functions:
const { principal } = useAccessControl();
const visible = items.filter((item) => hasPermission(principal, item.requiredPermissions));Stability
can and hasRole are memoised on the principal and the loading state, so they are stable between
renders and safe in a useEffect or useCallback dependency list. They change identity when the
principal changes — which is what you want, since the answer changes too.