From b16b8189f590cb28713288da7c08d577e6ddc27e Mon Sep 17 00:00:00 2001 From: Tony Novak Date: Thu, 11 May 2023 22:22:45 -0400 Subject: [PATCH] Enqueue premature calls to useNavigate() in react-router-6 adapter Fixes https://github.com/pbeshai/use-query-params/issues/211. --- .../src/index.ts | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/use-query-params-adapter-react-router-6/src/index.ts b/packages/use-query-params-adapter-react-router-6/src/index.ts index c1bb6ab..2c8d027 100644 --- a/packages/use-query-params-adapter-react-router-6/src/index.ts +++ b/packages/use-query-params-adapter-react-router-6/src/index.ts @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useCallback, useContext, useEffect, useRef } from 'react'; import { UNSAFE_NavigationContext, useNavigate, @@ -10,6 +10,48 @@ import { QueryParamAdapterComponent, } from 'use-query-params'; +// Prevent premature calls to useNavigate() from failing with "You should call +// navigate() in a React.useEffect(), not when your component is first +// rendered". +// +// This warning happens even when navigate() is called from a useEffect(), +// because that useEffect is called before useNavigate's internal useEffect +// (parents' effects are invoked after children's effects). +// +// To get around this, enqueue premature useNavigate() calls, and process the +// queue in a useEffect() (which is called *after* useNavigate's effect). +// +// See: https://github.com/pbeshai/use-query-params/issues/211 +function useNavigateDeferred() { + const realNavigate = useNavigate(); + const navigateState = useRef({ isValid: false, navigate: realNavigate, queue: [] }); + + if(navigateState.current.navigate !== realNavigate) { + navigateState.current.isValid = false + } + + const navigate = useCallback((to: any, options: any) => { + if(navigateState.current.isValid) { + navigateState.current.navigate(to, options); + } + else { + navigateState.current.queue.push([to, options]); + } + }, []) + + useEffect(() => { + navigateState.current.isValid = true + navigateState.current.navigate = realNavigate + + while(navigateState.current.queue.length > 0) { + const [to, options] = navigateState.current.queue.shift() + navigateState.current.navigate(to, options) + } + }, [realNavigate]) + + return navigate +} + /** * Query Param Adapter for react-router v6 */ @@ -22,10 +64,11 @@ export const ReactRouter6Adapter: QueryParamAdapterComponent = ({ // useLocation() output in case of some kind of breaking change we miss. // see: https://github.com/remix-run/react-router/blob/f3d87dcc91fbd6fd646064b88b4be52c15114603/packages/react-router-dom/index.tsx#L113-L131 const { navigator } = useContext(UNSAFE_NavigationContext); - const navigate = useNavigate(); + const navigate = useNavigateDeferred() const router = useContext(UNSAFE_DataRouterContext)?.router; const location = useLocation(); + const adapter: QueryParamAdapter = { replace(location) { navigate(location.search || '?', {