英文:
Cannot use useNavigate in custom hook with createBrowserRouter
问题
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 advantage 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 to another issue that I posted 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 modifying 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
英文:
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 <Router> 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
答案1
得分: 1
// `useUser`钩子只能在由路由器提供的路由上下文中调用,因此需要将其移动。
> 第二种方法是修改布局,使布局导入钩子并接收类似权限(在权限的情况下)和条件(在我需要时)的参数。
这是我建议的方法。`useUser`钩子已经在我假设的单个全局`AuthContext`值中读取数据,所以从我所看到的情况来看,将`useUser`钩子调用推送到需要它的组件中是完全安全的。
更新布局组件以访问`useUser`钩子值并将静态数据作为props传递。
```tsx
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 }} />
);
}
相应地更新布局路由组件。CommonLayout
现在不再消耗任何props,而AuthLayout
将传递一个角色/权限数组。移除了useUser
钩子调用。
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} />;
}
英文:
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} />;
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论