-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Bug]: Make setSearchParams stable? #9991
Comments
Also, calling |
I feel that all functions in hooks, especially those we’ll call in other hooks or as props to a component, are supposed to be stable. we just need to be able to get the latest states when these functions are called. The following is a simple solution to create stable functions: /* in a hook */
const fn = { /*... */ }
const fnRef = useRef(fn)
fnRef.current = fn
const stableFn = useCallback((...args) => fnRef.current(...args), [])
// then, we can use stableFn |
This issue has been automatically marked stale because we haven't received a response from the original author in a while 🙈. This automation helps keep the issue tracker clean from issues that are not actionable. Please reach out if you have more information for us or you think this issue shouldn't be closed! 🙂 If you don't do so within 7 days, this issue will be automatically closed. |
Please don't close, I literally ran into this issue today 🙏🏼 |
We are still affected by this bug. This issue should not be closed until a fix is published. |
I use useCallback and it works! const updateSearchParams = useCallback(setSearchParams, []); Please kindly consider to make updateFunction a stable reference :) |
Also, if multiple updates happen in the same render cycle, using the new value as a dependency of the callback causes it to no be provided within the prev param of the n+1 |
This is what I'm currently using as a workaround (following @zhengjitf stable function suggestion): import { useCallback, useEffect, useRef } from "react";
import { useSearchParams as useSearchParamsOrig } from "react-router-dom";
export function useSearchParams() {
const [searchParams, setSearchParams] = useSearchParamsOrig();
// Store a reference to the setter
const setSearchParamsRef = useRef(setSearchParams);
// Update the reference when the setter changes
useEffect(() => {
setSearchParamsRef.current = setSearchParams;
}, [setSearchParams]);
// Return a stable setter (which has no dep on searchParams)
const setSearchParamsStable = useCallback(
(...args: Parameters<typeof setSearchParams>) =>
setSearchParamsRef.current(...args),
[]
);
return { searchParams, setSearchParams: setSearchParamsStable };
} in case anybody is looking for the same workaround. It works for my use case, but I've not tested corner cases. |
@lucabergamini useEffect is no good, it would be best at the root off the component as the ref would be instantly updated instead of waiting for the render cycle to end in order to run the effects. |
Do you mean a |
@lucabergamini yes |
I ran into this same issue today - and since it was causing React rendering errors in the console, I decided to go ahead and put together a fix: #10740 |
Thank you! Hope to see this merged soon! I ran into the issue today, absolutely unexpected and seems very dangerous. |
What is the progress on this? Will the fix be merged? |
I ran into this same issue today as well. |
it seems like a big foot gun, I noticed a bug in my app when calling |
any update? or does anyone know of an alternative to |
I encountered the same problem recently, I always assumed that the setter was stable |
Currently I see two issues with
Setters are usually imperative, not reactive. That is why React's |
Hi, this is still an issue @kentcdodds any plans to actually change this behaviour? |
I'm not on the team so I don't know |
@ryanflorence @mjackson There have been already several attempts to fix the behaviour over more than a year now without success
could you please state your reasons against merging #10740 so we can move forward. |
For those who need to fix this issue ASAP while we wait, we've created our own hook (drop-in replacement): import {
type SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
import { useLocation, useNavigate } from '@remix-run/react' // switch to `react-router-dom` if using React Router
export default function useSearchParams() {
const location = useLocation()
const navigate = useNavigate()
const searchParams = useMemo(
() => new URLSearchParams(location.search),
[location.search],
)
const searchParamsRef = useRef(searchParams)
useEffect(() => {
searchParamsRef.current = searchParams
}, [searchParams])
const setSearchParams = useCallback(
(setter: SetStateAction<URLSearchParams>) => {
const newParams =
typeof setter === 'function' ? setter(searchParamsRef.current) : setter
navigate(`?${newParams}`)
},
[navigate],
)
return [searchParams, setSearchParams] as const
} |
export default function useSearchParams() {
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useState(
() => new URLSearchParams(location.search),
);
const skipFirstNavigate = useRef<boolean>(false);
useEffect(() => {
if (!skipFirstNavigate.current) {
skipFirstNavigate.current = true;
return;
}
navigate(`?${searchParams}`);
}, [navigate, searchParams]);
return [searchParams, setSearchParams] as const;
} |
You can download the project, make your modification, and try running the unit tests against it. It's been a minute - but if I recall correctly, setSearchParams allows you to perform partial updates to the parameters. IE, you don't need to pass all of the search params every time. Thus it is necessary to maintain a reference to the old search params in order to build the new search params - only overwriting the supplied values. |
ah i see. mine doesn't work with multiple params as the previous state only knows the the current params the last time that specific piece of state changed
|
@buzinas i had an issue with this version of the method when i made 2 changes to the url in quick succession in different hooks. They both acted off the same previous state. The example where this occurred is having a perPage and offset value on a paginated page, and changing the perPage value causes offset to reset back to 0. My solution was to move the reading of the current query variables as close as possible to the setting of the new ones, such that it's all synchronous, rather than trying to integrate it with the react lifecycle. This removes the option to pass the previous state to the setter, and requires you to pull export function useSearchParams() {
const location = useLocation();
const navigate = useNavigate();
const searchParams = useMemo(
() => new URLSearchParams(location.search),
[location.search],
);
const setSearchParams = useCallback(
(getNewParams: () => URLSearchParams) => {
navigate(`?${getNewParams()}`);
},
[navigate],
);
return [searchParams, setSearchParams] as const;
//example usage of setSearchParams
setSearchParams(() => {
const searchParams = new URLSearchParams(document.location.search);
searchParams.set('foo', 'bar');
return searchParams;
}); |
What version of React Router are you using?
6.7.0
Steps to Reproduce
setSearchParams
in anything that needs a dependency array and add it to avoid issues.setSearchParams
in the dependency array triggers the useEffect any time the searchParams change (for example) to run againExpected Behavior
I expect to be able to use
setSearchParams
in auseEffect
and not have thatuseEffect
re-run just because search params changed.Actual Behavior
This is happening because
setSearchParams
allows you to pass a function callback which receives the currentsearchParams
sosetSearchParams
changes whenever thesearchParams
change. This is a nice API, but leads to unexpected re-runs of things usingsetSearchParams
.An alternative is to use a ref to keep track of the latest
searchParams
and pass thatref.current
instead sosearchParams
doesn't need to be listed in thenextInit
call. I can't think of a scenario where this would break anything.The code I'm talking about is here:
react-router/packages/react-router-dom/index.tsx
Lines 872 to 881 in 0ce0e4c
The text was updated successfully, but these errors were encountered: