无法在使用createBrowserRouter的自定义钩子中使用useNavigate。

huangapple go评论62阅读模式
英文:

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) =&gt; {
			sessionStorage.setItem(&#39;access_token&#39;, tokenResponse.access_token)
			sessionStorage.setItem(&#39;refresh_token&#39;, tokenResponse.refresh_token)
			setToken(tokenResponse.access_token)
			const user = await getUserProfile()
			setUser(user)
			sessionStorage.setItem(&#39;userData&#39;, JSON.stringify(user))
	 		navigate(&#39;/home&#39;)
		},
		onError: () =&gt; {
			sessionStorage.removeItem(&#39;access_token&#39;)
		}
	})

	const { mutate: logout } = useMutation({
		mutationFn: signOut,
		onMutate: () =&gt; {
			sessionStorage.clear()
			setToken(null)
			setUser(null)
		},
		onSuccess: async () =&gt; {
			navigate(&#39;/login&#39;)
		}
	})

	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 &lt;BrowserRouter&gt; 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) =&gt; {
	return (
		&lt;div className=&#39;flex flex-col h-screen&#39;&gt;
			{isAllowed ? &lt;Navbar /&gt; : &lt;GuestNavbar /&gt;}
			&lt;main className=&#39;flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800&#39;&gt;
				&lt;Outlet /&gt;
			&lt;/main&gt;
			&lt;Footer /&gt;
		&lt;/div&gt;
	)
}

export const AuthLayout = ({ isAllowed, redirectPath, children }: Props) =&gt; {
	const location = useLocation()

	return isAllowed ? (
		children ?? &lt;Outlet /&gt;
	) : (
		&lt;Navigate to={redirectPath} replace state={{ from: location }} /&gt;
	)
}

And the new approach that I want:

export function App() {
	const { user, userPermissions } = useUser()
	const router = createBrowserRouter(
		createRoutesFromElements(
			&lt;&gt;
				&lt;Route element={&lt;CommonLayout isAllowed={!!user} /&gt;}&gt;
					&lt;Route path=&#39;login&#39; element={&lt;Login /&gt;} /&gt;
					&lt;Route element={&lt;AuthLayout isAllowed={userPermissions.includes(&#39;PERMISSION_1&#39;)} redirectPath=&#39;/login&#39; /&gt;}&gt;
						&lt;Route
							path=&#39;home&#39;
							element={
								&lt;&gt;
									&lt;Breadcrumbs /&gt;
									&lt;h1&gt;Home&lt;/h1&gt;
								&lt;/&gt;
							}
							handle={{ crumb: () =&gt; &#39;Home&#39; }}
						/&gt;
						&lt;Route
							path=&#39;tracking&#39;
							element={
								&lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
									&lt;Breadcrumbs /&gt;

									&lt;Tracking /&gt;
								&lt;/Suspense&gt;
							}
							handle={{ crumb: () =&gt; &#39;Tracking&#39; }}
						/&gt;
						&lt;Route
							path=&#39;cleaning&#39;
							element={
								&lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
									&lt;Breadcrumbs /&gt;

									&lt;Cleaning /&gt;
								&lt;/Suspense&gt;
							}
						/&gt;
						&lt;Route
							path=&#39;admin&#39;
							element={
								&lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
									&lt;Breadcrumbs /&gt;
									&lt;Admin /&gt;
								&lt;/Suspense&gt;
							}
							handle={{ crumb: () =&gt; &#39;Admin&#39; }}
						/&gt;
					&lt;/Route&gt;
				&lt;/Route&gt;
				&lt;Route path=&#39;*&#39; element={&lt;Navigate to=&#39;/home&#39; replace /&gt;} /&gt;
			&lt;/&gt;
		)
	)
	return &lt;RouterProvider router={router} /&gt;
}

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 = () =&gt; {
  const { user } = useUser();

  return (
    &lt;div className=&#39;flex flex-col h-screen&#39;&gt;
      {!!user ? &lt;Navbar /&gt; : &lt;GuestNavbar /&gt;}
      &lt;main className=&#39;flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800&#39;&gt;
        &lt;Outlet /&gt;
      &lt;/main&gt;
      &lt;Footer /&gt;
    &lt;/div&gt;
  )
}
export const AuthLayout = ({ children, redirectPath, roles = [] }: Props) =&gt; {
  const location = useLocation();
  const { userPermissions } = useUser();

  const isAllowed = userPermissions.some(permission =&gt; roles.includes(permission));

  return isAllowed ? (
    children ?? &lt;Outlet /&gt;
  ) : (
    &lt;Navigate to={redirectPath} replace state={{ from: location }} /&gt;
  );
}

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(
      &lt;&gt;
        &lt;Route element={&lt;CommonLayout /&gt;}&gt;
          &lt;Route path=&#39;login&#39; element={&lt;Login /&gt;} /&gt;
          &lt;Route
            element={(
              &lt;AuthLayout roles={[&#39;PERMISSION_1&#39;]} redirectPath=&#39;/login&#39; /&gt;
            )}
          &gt;
            &lt;Route
              path=&#39;home&#39;
              element={
                &lt;&gt;
                  &lt;Breadcrumbs /&gt;
                  &lt;h1&gt;Home&lt;/h1&gt;
                &lt;/&gt;
              }
              handle={{ crumb: () =&gt; &#39;Home&#39; }}
            /&gt;
            &lt;Route
              path=&#39;tracking&#39;
              element={
                &lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
                  &lt;Breadcrumbs /&gt;
                  &lt;Tracking /&gt;
                &lt;/Suspense&gt;
              }
              handle={{ crumb: () =&gt; &#39;Tracking&#39; }}
            /&gt;
            &lt;Route
              path=&#39;cleaning&#39;
              element={
                &lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
                  &lt;Breadcrumbs /&gt;
                  &lt;Cleaning /&gt;
                &lt;/Suspense&gt;
              }
            /&gt;
            &lt;Route
              path=&#39;admin&#39;
              element={
                &lt;Suspense fallback={&lt;&gt;...&lt;/&gt;}&gt;
                  &lt;Breadcrumbs /&gt;
                  &lt;Admin /&gt;
                &lt;/Suspense&gt;
              }
              handle={{ crumb: () =&gt; &#39;Admin&#39; }}
            /&gt;
          &lt;/Route&gt;
        &lt;/Route&gt;
        &lt;Route path=&#39;*&#39; element={&lt;Navigate to=&#39;/home&#39; replace /&gt;} /&gt;
      &lt;/&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

huangapple
  • 本文由 发表于 2023年6月1日 16:59:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76380245.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定