On every project you need to protect routes. Some are for authenticated users only, some for admins, some for guests only. You write a PrivateRoute. Then a role check hook. Then a HasAccess component. You move to the next project — and write it all again.
The logic is always the same. The implementation is always different.
react-protected standardizes this. You describe what each route requires. The library checks access and redirects.
Three packages
@react-protected/core — framework-agnostic access-control logic. No dependency on React or any router.
@react-protected/react — React context (AccessProvider), hooks (useHasAccess), and HasAccess component.
@react-protected/react-router — adapter for React Router. Includes everything from @react-protected/react — no need to install both.
Installation
For React Router projects:
npm install @react-protected/react-router
For React projects without React Router:
npm install @react-protected/core @react-protected/react
Config-based routing (recommended)
// router.ts
import { createAccessRouter } from '@react-protected/react-router'
import { useAuthStore } from './entities/auth'
export const router = createAccessRouter(
[
{ path: '/', element: <HomePage /> },
{ path: '/login', element: <LoginPage />, access: 'guest-only' },
{ path: '/dashboard', element: <DashboardPage />, access: 'authenticated' },
{ path: '/admin', element: <AdminPage />, access: 'authenticated', roles: ['admin'] },
{
path: '/contracts',
element: <ContractsPage />,
access: 'authenticated',
permissions: ['contracts:read'],
},
{ path: '/403', element: <Page403 /> },
],
{
getUser: () => useAuthStore.getState().user,
hasRole: (user, roles) => roles.some((role) => user.roles.includes(role)),
hasPermission: (user, permissions) =>
permissions.every((p) => user.permissions.includes(p)),
loginPath: '/login',
forbiddenPath: '/403',
defaultPath: '/dashboard',
callbackUrlParam: 'next',
}
)
// App.tsx
import { RouterProvider } from 'react-router-dom'
export const App = () => <RouterProvider router={router} />
JSX routes
import { Route, Routes } from 'react-router-dom'
import { AccessProvider, AccessRoute } from '@react-protected/react-router'
const App = () => (
<AccessProvider
getUser={() => useAuthStore.getState().user}
hasRole={(user, roles) => roles.some((role) => user.roles.includes(role))}
loginPath="/login"
forbiddenPath="/403"
defaultPath="/dashboard"
callbackUrlParam="next"
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route
path="/login"
element={
<AccessRoute access="guest-only">
<LoginPage />
</AccessRoute>
}
/>
<Route
path="/dashboard"
element={
<AccessRoute access="authenticated">
<DashboardPage />
</AccessRoute>
}
/>
</Routes>
</AccessProvider>
)
Roles, permissions, or both
RBAC — pass roles to the route. You define the check logic yourself: OR, AND, hierarchy — whatever your domain requires.
ABAC — pass permissions. Works the same way.
Both at once — the check passes only when both conditions are met.
Guarding UI elements
Route protection is not enough. HasAccess and useHasAccess let you hide buttons, fields, or any UI element based on role or permission — no route change required.
import { HasAccess, useHasAccess } from '@react-protected/react-router'
// Component
const Toolbar = () => (
<nav>
<HasAccess roles={['admin']}>
<button>Delete user</button>
</HasAccess>
</nav>
)
// Hook
const ExportButton = () => {
const canExport = useHasAccess({ permissions: ['reports:export'] })
return canExport ? <button>Export</button> : null
}
Callback URL
When callbackUrlParam is set, unauthenticated users are redirected with the current path preserved:
/dashboard?tab=overview → /login?next=%2Fdashboard%3Ftab%3Doverview
Handle the return in your login page:
const [params] = useSearchParams()
navigate(params.get('next') ?? '/dashboard', { replace: true })
If you want to suppress the callback URL after an explicit logout:
<AccessProvider
callbackUrlParam="next"
shouldAddCallbackUrl={() => !authStore.getState().loggedOut}
...
>
Not using React Router?
Use @react-protected/core directly with any router. Here's TanStack Router:
import { createGuard } from '@react-protected/core'
const guard = createGuard({
getUser: () => useAuthStore.getState().user,
hasRole: (user, roles) => roles.some((role) => user.roles.includes(role)),
})
const dashboardRoute = createRoute({
path: '/dashboard',
beforeLoad: ({ location }) => {
const result = guard.check({ access: 'authenticated' })
if (!result.allowed) {
throw redirect({
to: result.reason === 'unauthenticated' ? '/login' : '/403',
search: { next: location.pathname },
})
}
},
component: DashboardPage,
})
👉 github.com/astakhovaskold/react-protected
Issues and stars are welcome.
Top comments (0)