I have this custom hook for login and logout
export function useUser() {
const { token, setToken, user, setUser } = useContext(AuthContext)
const navigate = useNavigate()
const {
mutate: login,
isLoading: isLoginLoading,
isError: hasLoginError,
} = useMutation({
mutationFn: signIn,
onSuccess: async (tokenResponse) => {
sessionStorage.setItem('access_token', tokenResponse.access_token)
sessionStorage.setItem('refresh_token', tokenResponse.refresh_token)
setToken(tokenResponse.access_token)
const user = await getUserProfile()
setUser(user)
sessionStorage.setItem('userData', JSON.stringify(user))
navigate('/home')
},
onError: () => {
sessionStorage.removeItem('access_token')
}
})
const { mutate: logout } = useMutation({
mutationFn: signOut,
onMutate: () => {
sessionStorage.clear()
setToken(null)
setUser(null)
},
onSuccess: async () => {
navigate('/login')
}
})
return {
login,
logout,
isLoginLoading,
hasLoginError,
userPermissions: token ? (jwt_decode(token) as TokenDecoded).authorities : [],
user
}
}
And I want to take advantange of the new data router with react-router-dom createBrowserRouter
and try to create a breadcrumb. Before this I use <BrowserRouter>
and it works fine, but with this new approach, and because of the use of protected routes, I get the error.
useNavigate() may be used only in the context of a component.
I perfectly understand the error, but I don't know how to make it work. This is related with another issue that I post this issue and with a similar issue similar issue. Here is my code :
CommonLayout.tsx and AuthLayout.tsx
export const CommonLayout = ({ isAllowed }: Props) => {
return (
<div className='flex flex-col h-screen'>
{isAllowed ? <Navbar /> : <GuestNavbar />}
<main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
<Outlet />
</main>
<Footer />
</div>
)
}
export const AuthLayout = ({ isAllowed, redirectPath, children }: Props) => {
const location = useLocation()
return isAllowed ? (
children ?? <Outlet />
) : (
<Navigate to={redirectPath} replace state={{ from: location }} />
)
}
And the new approach that I want:
export function App() {
const { user, userPermissions } = useUser()
const router = createBrowserRouter(
createRoutesFromElements(
<>
<Route element={<CommonLayout isAllowed={!!user} />}>
<Route path='login' element={<Login />} />
<Route element={<AuthLayout isAllowed={userPermissions.includes('PERMISSION_1')} redirectPath='/login' />}>
<Route
path='home'
element={
<>
<Breadcrumbs />
<h1>Home</h1>
</>
}
handle={{ crumb: () => 'Home' }}
/>
<Route
path='tracking'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Tracking />
</Suspense>
}
handle={{ crumb: () => 'Tracking' }}
/>
<Route
path='cleaning'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Cleaning />
</Suspense>
}
/>
<Route
path='admin'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Admin />
</Suspense>
}
handle={{ crumb: () => 'Admin' }}
/>
</Route>
</Route>
<Route path='*' element={<Navigate to='/home' replace />} />
</>
)
)
return <RouterProvider router={router} />
}
As you can see, I need the user
because some routes need it, but importing the hook throws the error. How can I avoid this?
The first approach is removing the useNavigate
from the hook, useUser.
A second one is modify the layouts, so that the layouts import the hooks and receive as params something like a permission (in case of permissions) and a condition (in case I need it)
Thanks in advance
The useUser
hook can only be called within a routing context provided by a router, so it will need to be moved.
A second one is modify the layouts, so that the layouts import the hooks and receive as params something like a permission (in case of permissions) and a condition (in case I need it).
This is the method I'd recommend. The useUser
hook is already reading what I assume is a single global AuthContext
value, so from what I can see it's completely safe to push the useUser
hook calls down the ReactTree to the components that need it.
Update the layout components to access the useUser
hook value and pass in the static data as props.
export const CommonLayout = () => {
const { user } = useUser();
return (
<div className='flex flex-col h-screen'>
{!!user ? <Navbar /> : <GuestNavbar />}
<main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
<Outlet />
</main>
<Footer />
</div>
)
}
export const AuthLayout = ({ children, redirectPath, roles = [] }: Props) => {
const location = useLocation();
const { userPermissions } = useUser();
const isAllowed = userPermissions.some(permission => roles.includes(permission));
return isAllowed ? (
children ?? <Outlet />
) : (
<Navigate to={redirectPath} replace state={{ from: location }} />
);
}
Update the layout route components accordingly. CommonLayout
doesn't consume any props now, and AuthLayout
is passed an array of roles/permissions. The useUser
hook call is removed.
export function App() {
const router = createBrowserRouter(
createRoutesFromElements(
<>
<Route element={<CommonLayout />}>
<Route path='login' element={<Login />} />
<Route
element={(
<AuthLayout roles={['PERMISSION_1']} redirectPath='/login' />
)}
>
<Route
path='home'
element={
<>
<Breadcrumbs />
<h1>Home</h1>
</>
}
handle={{ crumb: () => 'Home' }}
/>
<Route
path='tracking'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Tracking />
</Suspense>
}
handle={{ crumb: () => 'Tracking' }}
/>
<Route
path='cleaning'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Cleaning />
</Suspense>
}
/>
<Route
path='admin'
element={
<Suspense fallback={<>...</>}>
<Breadcrumbs />
<Admin />
</Suspense>
}
handle={{ crumb: () => 'Admin' }}
/>
</Route>
</Route>
<Route path='*' element={<Navigate to='/home' replace />} />
</>
)
);
return <RouterProvider router={router} />;
}