Skip to main content

react

React Provider, hooks, and declarative Can/Cannot components for duck-iam. Server-driven hydration with usePermissions for client-only flows.

Install

import { createAccessControl, createPermissionChecker } from '@gentleduck/iam/client/react'
import { createAccessControl, createPermissionChecker } from '@gentleduck/iam/client/react'

React is an optional peer dep. The factory accepts your React import to avoid a hard version dependency.


Setup

Create the access control system once at app initialization. Pass your React import to avoid a hard dependency.

// lib/access.tsx
import React from 'react'
import { createAccessControl } from '@gentleduck/iam/client/react'
 
export const { AccessProvider, useAccess, usePermissions, Can, Cannot } = createAccessControl(React)
// lib/access.tsx
import React from 'react'
import { createAccessControl } from '@gentleduck/iam/client/react'
 
export const { AccessProvider, useAccess, usePermissions, Can, Cannot } = createAccessControl(React)

Type the system to your action/resource/scope unions:

type Action = 'create' | 'read' | 'update' | 'delete'
type Resource = 'post' | 'comment' | 'team'
type Scope = 'org-1' | 'admin'
 
export const access = createAccessControl<Action, Resource, Scope>(React)
type Action = 'create' | 'read' | 'update' | 'delete'
type Resource = 'post' | 'comment' | 'team'
type Scope = 'org-1' | 'admin'
 
export const access = createAccessControl<Action, Resource, Scope>(React)

AccessProvider

Wrap your app (or a subtree) with the provider. Pass the permission map generated on the server.

// app/layout.tsx (Next.js example)
import { AccessProvider } from '@/lib/access'
import { getPermissions } from '@gentleduck/iam/server/next'
 
export default async function RootLayout({ children }) {
  const session = await auth()
  const permissions = session?.user
    ? await getPermissions(engine, session.user.id, [
        { action: 'create', resource: 'post' },
        { action: 'delete', resource: 'post' },
        { action: 'manage', resource: 'team' },
        { action: 'read', resource: 'analytics' },
      ])
    : {}
 
  return <AccessProvider permissions={permissions}>{children}</AccessProvider>
}
// app/layout.tsx (Next.js example)
import { AccessProvider } from '@/lib/access'
import { getPermissions } from '@gentleduck/iam/server/next'
 
export default async function RootLayout({ children }) {
  const session = await auth()
  const permissions = session?.user
    ? await getPermissions(engine, session.user.id, [
        { action: 'create', resource: 'post' },
        { action: 'delete', resource: 'post' },
        { action: 'manage', resource: 'team' },
        { action: 'read', resource: 'analytics' },
      ])
    : {}
 
  return <AccessProvider permissions={permissions}>{children}</AccessProvider>
}

The provider memoizes the can/cannot callbacks on the permission map identity, so re-renders are cheap.


useAccess hook

Read permissions from context in any component.

import { useAccess } from '@/lib/access'
 
function PostActions({ postId }: { postId: string }) {
  const { can, cannot } = useAccess()
 
  return (
    <div>
      {can('update', 'post') && <button>Edit</button>}
      {can('delete', 'post') && <button>Delete</button>}
      {cannot('manage', 'team') && <span>Contact an admin to manage teams</span>}
    </div>
  )
}
import { useAccess } from '@/lib/access'
 
function PostActions({ postId }: { postId: string }) {
  const { can, cannot } = useAccess()
 
  return (
    <div>
      {can('update', 'post') && <button>Edit</button>}
      {can('delete', 'post') && <button>Delete</button>}
      {cannot('manage', 'team') && <span>Contact an admin to manage teams</span>}
    </div>
  )
}

The hook returns:

PropertyTypeDescription
permissionsPermissionMapThe raw permission map
can(action, resource, resourceId?, scope?) -> booleanCheck if a permission is granted
cannot(action, resource, resourceId?, scope?) -> booleanCheck if a permission is denied

Can and Cannot components

Declarative permission gates for JSX.

import { Can, Cannot } from '@/lib/access'
 
function Dashboard() {
  return (
    <div>
      <Can action="read" resource="analytics">
        <AnalyticsPanel />
      </Can>
 
      <Can action="manage" resource="team" fallback={<UpgradePrompt />}>
        <TeamSettings />
      </Can>
 
      <Cannot action="create" resource="post">
        <p>You do not have permission to create posts.</p>
      </Cannot>
    </div>
  )
}
import { Can, Cannot } from '@/lib/access'
 
function Dashboard() {
  return (
    <div>
      <Can action="read" resource="analytics">
        <AnalyticsPanel />
      </Can>
 
      <Can action="manage" resource="team" fallback={<UpgradePrompt />}>
        <TeamSettings />
      </Can>
 
      <Cannot action="create" resource="post">
        <p>You do not have permission to create posts.</p>
      </Cannot>
    </div>
  )
}

Can props

PropTypeRequiredDescription
actionstringyesThe action to check
resourcestringyesThe resource type to check
resourceIdstringnoSpecific resource instance
scopestringnoScope for the check
childrenReactNodeyesRendered when allowed
fallbackReactNodenoRendered when denied (defaults to null)

Cannot props

Same as Can minus fallback. Renders children when the permission is denied.


usePermissions hook

Fetch permissions from a server endpoint on the client side. Useful for SPAs without server-side rendering.

import { usePermissions } from '@/lib/access'
 
function App() {
  const { can, loading, error } = usePermissions(
    () => fetch('/api/me/permissions').then((r) => r.json()),
    [], // dependency array, like useEffect
  )
 
  if (loading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
 
  return <div>{can('create', 'post') && <NewPostButton />}</div>
}
import { usePermissions } from '@/lib/access'
 
function App() {
  const { can, loading, error } = usePermissions(
    () => fetch('/api/me/permissions').then((r) => r.json()),
    [], // dependency array, like useEffect
  )
 
  if (loading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
 
  return <div>{can('create', 'post') && <NewPostButton />}</div>
}

The hook returns:

PropertyTypeDescription
permissionsPermissionMapThe fetched permission map
can(action, resource, resourceId?, scope?) -> booleanPermission checker
loadingbooleanTrue while the fetch is in progress
errorError or nullError from the fetch, if any

The hook handles race conditions internally: if the component unmounts or the dependency array changes before the fetch completes, stale results are discarded.

usePermissions does not abort in-flight requests — it only ignores late results. If you need true cancellation, use the standalone checker with your own AbortController flow.


Standalone checker

For code outside React components (utilities, event handlers, tests), use createPermissionChecker. It takes a PermissionMap and returns a checker object with can, cannot, and the raw permissions.

import { createPermissionChecker } from '@gentleduck/iam/client/react'
 
const checker = createPermissionChecker(permissionMap)
checker.can('delete', 'post') // boolean
checker.cannot('manage', 'team') // boolean
checker.permissions // the original PermissionMap
import { createPermissionChecker } from '@gentleduck/iam/client/react'
 
const checker = createPermissionChecker(permissionMap)
checker.can('delete', 'post') // boolean
checker.cannot('manage', 'team') // boolean
checker.permissions // the original PermissionMap

Use this when you don't need React context — for example, in route loaders, form validators, or analytics event handlers.


When to use what

NeedUse
Wrap whole tree with permissions from RSCAccessProvider + useAccess
Wrap whole tree with permissions from APIAccessProvider + usePermissions (or fetch in layout)
One-off check inside JSX<Can> / <Cannot>
Check inside utility codecreatePermissionChecker

React Server Components note

AccessProvider is a client component (uses useMemo/context). Mark the wrapper file with "use client" if you import it from a Server Component:

'use client'
import { AccessProvider } from '@gentleduck/iam/client/react'
// ...
'use client'
import { AccessProvider } from '@gentleduck/iam/client/react'
// ...

For server-side checks inside RSC/Server Actions, use checkAccess from @gentleduck/iam/server/next instead — that hits the engine directly without touching client context.