Skip to content

React Integration

Fire Shield provides hooks and components for React applications.

Installation

bash
npm install @fire-shield/react@3.1.1 @fire-shield/core@3.1.1

Setup

Basic Setup

typescript
// App.tsx
import { RBACProvider } from '@fire-shield/react'
import { RBAC } from '@fire-shield/core'
import { useState } from 'react'

// Initialize RBAC
const rbac = new RBAC()
rbac.createRole('admin', ['posts:*', 'users:*'])
rbac.createRole('editor', ['posts:read', 'posts:write'])
rbac.createRole('viewer', ['posts:read'])

function App() {
  const [user, setUser] = useState({
    id: '1',
    roles: ['editor']
  })

  return (
    <RBACProvider rbac={rbac} user={user}>
      <YourApp />
    </RBACProvider>
  )
}

Hooks

Hooks

Access RBAC functionality in your components using the dedicated hooks:

tsx
import { useRBAC, usePermission, useRole, useUser } from '@fire-shield/react'

function PostEditor() {
  const rbac = useRBAC()           // Returns RBAC instance directly
  const user = useUser()           // Returns RBACUser | null
  const canWrite = usePermission('posts:write')
  const canDelete = usePermission('posts:delete')
  const isAdmin = useRole('admin')

  const handleDelete = () => {
    if (!canDelete) {
      alert('No permission to delete')
      return
    }
    deletePost()
  }

  return (
    <div>
      <h2>Post Editor</h2>
      <p>Current user: {user?.id}</p>

      {canWrite && (
        <button onClick={createPost}>Create Post</button>
      )}

      {canDelete && (
        <button onClick={handleDelete}>Delete Post</button>
      )}

      {isAdmin && (
        <button onClick={openAdminPanel}>Admin Panel</button>
      )}
    </div>
  )
}

Hook API

typescript
// Returns RBAC instance from context
useRBAC(): RBAC

// Check if current user has permission - returns boolean
usePermission(permission: string): boolean

// Check if current user has role - returns boolean
useRole(role: string): boolean

// Get current user from context
useUser(): RBACUser | null

// Get full authorization result
useAuthorize(permission: string): AuthorizationResult

// Check if user has ALL specified permissions
useAllPermissions(...permissions: string[]): boolean

// Check if user has ANY of the specified permissions
useAnyPermission(...permissions: string[]): boolean

// Returns a function to explicitly deny a permission for the current user
useDenyPermission(): (permission: string) => void

// Returns a function to remove a previously denied permission
useAllowPermission(): (permission: string) => void

// Returns array of explicitly denied permissions for the current user
useDeniedPermissions(): string[]

// Check if a specific permission is explicitly denied for the current user
useIsDenied(permission: string): boolean

Components

Can Component

Conditionally render content based on permissions:

tsx
import { Can } from '@fire-shield/react'

function PostActions() {
  return (
    <div>
      <Can permission="posts:write">
        <button>Create Post</button>
      </Can>

      <Can permission="posts:delete" fallback={<p>No permission</p>}>
        <button>Delete Post</button>
      </Can>
    </div>
  )
}

Cannot Component

Inverse of Can component:

tsx
import { Cannot } from '@fire-shield/react'

function UpgradePrompt() {
  return (
    <Cannot permission="premium:access">
      <div className="upgrade-banner">
        <p>Upgrade to unlock premium features</p>
        <button>Upgrade Now</button>
      </div>
    </Cannot>
  )
}

RequirePermission Component

Throw error or show fallback if permission is missing:

tsx
import { RequirePermission } from '@fire-shield/react'

function AdminPanel() {
  return (
    <RequirePermission
      permission="admin:access"
      fallback={<div>Access Denied</div>}
    >
      <div>
        <h1>Admin Panel</h1>
        {/* Admin content */}
      </div>
    </RequirePermission>
  )
}

Denied Component

Render if the user has the permission explicitly denied:

tsx
import { Denied, NotDenied } from '@fire-shield/react'

function PostActions() {
  return (
    <div>
      {/* Shows children only when permission is explicitly denied */}
      <Denied permission="posts:delete">
        <p>Your delete access has been revoked.</p>
      </Denied>

      {/* Shows children when permission is NOT explicitly denied */}
      <NotDenied permission="posts:delete">
        <button>Delete Post</button>
      </NotDenied>
    </div>
  )
}

Deny / Allow Permissions at Runtime

tsx
import { useDenyPermission, useAllowPermission, useDeniedPermissions } from '@fire-shield/react'

function AdminControls() {
  const denyPermission = useDenyPermission()
  const allowPermission = useAllowPermission()
  const denied = useDeniedPermissions()

  return (
    <div>
      <p>Denied: {denied.join(', ')}</p>
      <button onClick={() => denyPermission('posts:delete')}>Revoke Delete</button>
      <button onClick={() => allowPermission('posts:delete')}>Restore Delete</button>
    </div>
  )
}

React Router Integration

Protected Routes

tsx
import { Navigate } from 'react-router-dom'
import { usePermission } from '@fire-shield/react'

function ProtectedRoute({ permission, children }) {
  const hasPermission = usePermission(permission)

  if (!hasPermission) {
    return <Navigate to="/unauthorized" replace />
  }

  return children
}

// Usage in router
<Routes>
  <Route
    path="/admin"
    element={
      <ProtectedRoute permission="admin:access">
        <AdminPage />
      </ProtectedRoute>
    }
  />
</Routes>

Route Guards

Note: React hooks cannot be called inside route loaders. Use the built-in ProtectedRoute component from @fire-shield/react for route-level protection:

tsx
import { ProtectedRoute } from '@fire-shield/react'

// Usage with react-router-dom
<Route
  path="/admin"
  element={
    <ProtectedRoute permission="admin:access" redirectTo="/unauthorized">
      <AdminLayout />
    </ProtectedRoute>
  }
/>

Updating User

Update user permissions dynamically by passing a new user prop to RBACProvider:

tsx
import { useState } from 'react'
import { RBACProvider } from '@fire-shield/react'
import { useUser } from '@fire-shield/react'

// In a parent component, manage user state and pass it to RBACProvider
function App() {
  const [user, setUser] = useState({ id: '1', roles: ['editor'] })

  const switchToAdmin = () => {
    setUser({ id: 'admin-1', roles: ['admin'] })
  }

  const switchToViewer = () => {
    setUser({ id: 'viewer-1', roles: ['viewer'] })
  }

  return (
    <RBACProvider rbac={rbac} user={user}>
      <div>
        <p>Current user: {user?.id}</p>
        <button onClick={switchToAdmin}>Switch to Admin</button>
        <button onClick={switchToViewer}>Switch to Viewer</button>
      </div>
    </RBACProvider>
  )
}

TypeScript Support

Full TypeScript support with type inference:

typescript
import { usePermission, useRole, useUser } from '@fire-shield/react'
import type { RBACUser } from '@fire-shield/core'

// Type-safe user
const user: RBACUser = {
  id: 'user-123',
  roles: ['editor']
}

// Type-safe hooks
const canWrite = usePermission('posts:write')   // boolean
const isEditor = useRole('editor')              // boolean

Best Practices

1. Use Components for JSX

tsx
// ✅ Good: Declarative and clean
<Can permission="posts:delete">
  <button>Delete</button>
</Can>

// ❌ Avoid: Less readable
{canDelete && <button>Delete</button>}

2. Use Hooks for Logic

tsx
// ✅ Good: Clear permission check
import { usePermission } from '@fire-shield/react'

const canDelete = usePermission('posts:delete')

const handleDelete = () => {
  if (!canDelete) {
    showError('No permission')
    return
  }
  deletePost()
}

3. Memoize Permission Checks

tsx
import { usePermission } from '@fire-shield/react'

function ExpensiveComponent() {
  const canWrite = usePermission('posts:write')
  const canDelete = usePermission('posts:delete')
  const canPublish = usePermission('posts:publish')

  return (
    // Use permission values
  )
}

4. Handle Loading States

tsx
import { useUser, usePermission } from '@fire-shield/react'

function UserProfile() {
  const user = useUser()
  const canEdit = usePermission('profile:edit')

  if (!user) {
    return <div>Loading...</div>
  }

  return (
    <div>
      <h1>{user.id}</h1>
      {canEdit && <EditButton />}
    </div>
  )
}

Server-Side Rendering (SSR)

Next.js Integration

For Next.js, use the dedicated @fire-shield/next package which provides server-side support.

Other SSR Frameworks

Provide initial user state from server:

tsx
// server.tsx
const initialUser = await getUserFromSession(req)

const html = renderToString(
  <RBACProvider rbac={rbac} user={initialUser}>
    <App />
  </RBACProvider>
)

Testing

Testing Components with RBAC

tsx
import { render, screen } from '@testing-library/react'
import { RBACProvider } from '@fire-shield/react'
import { RBAC } from '@fire-shield/core'

function renderWithRBAC(component, user) {
  const rbac = new RBAC()
  rbac.createRole('admin', ['posts:*'])
  rbac.createRole('viewer', ['posts:read'])

  return render(
    <RBACProvider rbac={rbac} user={user}>
      {component}
    </RBACProvider>
  )
}

test('shows delete button for admin', () => {
  renderWithRBAC(
    <PostActions />,
    { id: '1', roles: ['admin'] }
  )

  expect(screen.getByText('Delete Post')).toBeInTheDocument()
})

test('hides delete button for viewer', () => {
  renderWithRBAC(
    <PostActions />,
    { id: '2', roles: ['viewer'] }
  )

  expect(screen.queryByText('Delete Post')).not.toBeInTheDocument()
})

Examples

Blog Post Management

tsx
import { Can, usePermission } from '@fire-shield/react'

function PostCard({ post }) {
  const canPublish = usePermission('posts:publish')

  return (
    <div className="post-card">
      <h3>{post.title}</h3>
      <p>{post.excerpt}</p>

      <div className="actions">
        <Can permission="posts:read">
          <button>Read More</button>
        </Can>

        <Can permission="posts:write">
          <button>Edit</button>
        </Can>

        <Can permission="posts:delete">
          <button>Delete</button>
        </Can>

        {canPublish && !post.published && (
          <button>Publish</button>
        )}
      </div>
    </div>
  )
}

User Management Dashboard

tsx
import { RequirePermission, Can } from '@fire-shield/react'

function UserManagement() {
  return (
    <RequirePermission permission="users:read">
      <div>
        <h1>User Management</h1>

        <Can permission="users:create">
          <button>Add New User</button>
        </Can>

        <UserList />

        <Can permission="users:export">
          <button>Export Users</button>
        </Can>
      </div>
    </RequirePermission>
  )
}

Next Steps