Skip to Content
React AuthorizationGetting Started

Unreleased changes

These pages document main, which has changes 0.1.2 does not. npm install @evanion/react-acl gives you 0.1.2, documented here. Values on these pages are produced by running the current source on every build.

Getting started

By the end of this page your server hands its matrix to PolicyProvider, and an Edit button under it renders for the seller who listed the game and for nobody else.

Before you install react-acl

  • React 18 or 19, the package’s only peer dependency, and react-dom.
  • Node 20 or newer and an ESM project, since @evanion/react-acl has no CommonJS build.
  • Rules written with policy() from @evanion/acl. Getting started with acl writes a first policy.
  • A subject your server already resolves: the signed-in user, as a plain object.

Install react-acl

Install react-acl and @evanion/acl, the package it reads the rules from, with the line for your package manager.

npm install @evanion/react-acl @evanion/acl
yarn add @evanion/react-acl @evanion/acl
pnpm add @evanion/react-acl @evanion/acl

Mount PolicyProvider in a client component

react-acl’s PolicyProvider takes three props. access is the evaluator, subject is whoever is signed in, and context carries now, the instant every time window in the rules is read against. context is a plain object and has nothing to do with React context. ShopAccess below takes the matrix the server sent and builds the evaluator with hydratePolicy:

'use client'; import { } from 'react'; import type { } from 'react'; import { } from '@evanion/acl'; import type { Matrix } from '@evanion/acl'; import { } from '@evanion/react-acl'; type = { : string; : 'customer' | 'bookseller' | 'owner' }; export function ({ , , , , }: { : Matrix; : ; : string; : ; }) { // hydratePolicy validates, deep-clones and deep-freezes the document, so it // runs once per matrix rather than once per render. const = (() => (), []); const = (() => ({ }), []); return ( < ={} ={} ={}> {} </> ); }

ShopAccess is written this way on purpose:

  • Both useMemo calls are required. PolicyProvider memoises on the identity of access, subject and context, and an object built during render is a new identity on every pass.
  • now is an ISO string, and it stays a string. The hooks key their memos on it, and a Date is a new object on every render, so a Date re-evaluates the rules on each pass.
  • 'use client' makes ShopAccess a client component, so a server component can render it and pass the matrix, the shopper and now as props.

Hand the server’s matrix to react-acl

The server builds the rules once with policy(). Its third type argument, { listing: 'edit' }, lists the actions listing accepts. A kind with no entry there accepts only create, read, update and delete, and an entry replaces those four, so this policy accepts edit alone. Action | 'edit', with the Action type from @evanion/acl, keeps the four and adds edit.

For each request the server renders ShopAccess, and ShopAccess hands the rules to react-acl. access.matrix is the JSON document policy() built, the matrix, and ShopAccess hydrates it into the browser’s own evaluator. The server also resolves the shopper and reads now once for the render, so the server’s HTML and the browser’s first render read the rules at the same instant. EditControl, and the Listing type it takes, are the button the Overview draws with useCan, and renderToStaticMarkup from react-dom/server stands in for your framework’s server render:

// On the server, once per process: the seller who listed a listing edits it. const = <, { : }, { : 'edit' }>() .('listing', () => .('edit', .('object.sellerId', 'subject.id'))) .(); const : = { : 'brass-birmingham', : 'mika', : 'draft', }; // On the server, per request. In production `now` is // new Date().toISOString(), read once for the whole render. const = (: ) => ( < ={.} ={} ="2026-09-25T09:00:00.000Z" > < ={} /> </>, ); const : = { : 'mika', : 'bookseller' }; const : = { : 'jo', : 'owner' }; (); // -> '<button type="button">Edit listing</button>' (); // -> ''

Each // -> comment is the HTML its line returns. Mika listed the game and gets the button. Jo gets an empty string, because EditControl returned null and React renders nothing for it. Render the same tree from your own server for two shoppers, and react-acl is working in your project when the HTML matches.

Every component under the provider follows two conventions:

  • Gate on allowed and nothing else. reason explains the answer, so a component that branches on it is writing a rule of its own.
  • Keep a PolicyProvider above every hook. useCan throws without one, and the message names the provider.

An app with no server render fetches the same three values. An endpoint returns access.matrix, the shopper and now as JSON, and the client mounts ShopAccess once the response arrives. Until then the client renders no component that calls a hook.

Switch the signed-in shopper under one react-acl provider

The tree below mounts one react-acl PolicyProvider with hydratePolicy, as ShopAccess does, over four rules. ShopPage, ListingView and ActionBar pass nothing. The shop’s roles are customer, bookseller and owner. Sam is the customer, Mika the bookseller and Jo the owner:

  • Everybody may review a listing.
  • The seller who listed it may edit it. Mika listed this one.
  • Only the shop’s owner may publish it.
  • Nobody may edit a published listing.

Each box shows the hook call it makes and what came back:

PolicyProvidersubject={"Sam Reyes"}

ShopPage

ListingView

ActionBar

ReviewControluseCan('listing', 'review', listing)allowed

EditControluseCan('listing', 'edit', listing)no-rule-matched

PublishControluseCan('listing', 'publish', listing)no-rule-matched

what the controls draw

  • Review
  • Edit
  • Publish

ShopPage, ListingView and ActionBar take no authorization props and pass none down. The listing is draft.

Every badge is a browser deciding what to draw. A control that is not rendered is a control that is not rendered; the request behind it is still a URL, and the server decides again.

A refused badge shows the decision’s reason: no-rule-matched when no rule allows, and denied when a rule refuses. Review is allowed for all three shoppers. Sam sees no-rule-matched on Edit and Publish. Mika sees Edit allowed and no-rule-matched on Publish. Jo sees Publish allowed and no-rule-matched on Edit.

Publish the listing and Edit shows denied for all three, because the fourth rule refuses and a refusal wins over any allow. Only EditControl changes, since the fourth rule is the only one that reads the listing’s status.

Enforce the edit behind the react-acl button

EditControl now shows the Edit button to the seller who listed the game and hides it from everyone else. On Which side decides you write editListing, the handler behind the button, which asks access.can the same question where the shopper cannot change the rules.

Last updated on