Skip to Content
Piplup
@piplup/react-aclComponents

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&apos;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;
  • Can renders its loading prop. It does not render fallback, so users don't see an "access denied" message flash before the data arrives;
  • useAccessControl().loading is true, 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 undefined but the checks should still be suspended;
  • undefined is 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

On this page