wouter
wouter copied to clipboard
Potential `useSearch` bug not triggering updates when used with `urql`
Greetings, I've been scratching my head over this issue for quite awhile.
Following #368, I've been using the useSearchParams
hook and it has been working fine, until I started integrating with urql
's useQuery
.
Issue
useSearch
does not rerender if it begins with query params and revisited. For example, if I land on the page with URL
http://localhost:5173/?tab=1
I cannot go back to original tab after switching to another tab. (URL has been updated to ?tab=1
, but React does not rerender the components. You may see the video below:
https://github.com/molefrog/wouter/assets/40173716/58bb4027-6457-40d3-b780-4066fefc7eae
Reproduction Steps
-
Clone https://github.com/junwen-k/react-router-wouter
git clone https://github.com/junwen-k/react-router-wouter cd react-router-wouter
-
Install dependencies
npm install
-
Start Server
npm start
-
Experiment with the page, with default
?tab=1
triggers the issue
Context
Suspect pushState
event does not trigger, hence React does not rerenders the component.
React Router's version works perfectly, so I suspect this is something to do with wouter
.
After playing around and some debugging, I notice that when I do
packages/wouter/src/use-browser-location.js
export const useSearch = ({ ssrSearch = "" } = {}) => {
return useLocationProperty(
() => location.search,
() => ssrSearch
);
};
Instead of
const currentSearch = () => location.search;
export const useSearch = ({ ssrSearch = "" } = {}) =>
useLocationProperty(currentSearch, () => ssrSearch);
The issue seems to be fixed. Could this be a potential bug with how the library uses useSyncExternalStore
?
https://github.com/molefrog/wouter/assets/40173716/0dd64042-fbdf-43dd-ac10-d48ea428035f
If this is really the cause of the bug, does that mean that usePathname
and useHistoryState
could potentially having the same exact issue as well?
export const useSearch = ({ ssrSearch = "" } = {}) =>
useLocationProperty(
() => location.search,
() => ssrSearch
);
// ...
export const usePathname = ({ ssrPath } = {}) =>
useLocationProperty(
() => location.pathname,
ssrPath ? () => ssrPath : location.pathname
);
// ...
export const useHistoryState = () =>
useLocationProperty(
() => history.state,
() => null
);
I am guessing, when we define
const currentSearch = () => location.search;
The value of currentSearch
got "memorised" during initialisation, which causes this issue...
Any thoughts / comments are welcomed!
@junwen-k Hi! My guess it that useSearchParams
from RR isn't compatible with wouter and it requires a separate implementation which is wouter-specific. I have an idea, as an experiment, use this dummy hook instead:
const useSearchParams = () => {
const [,navigate] = useLocation()
const search = useSearch()
const params = new URLSearchParams(window.location.search)
const setSearchParams = (obj) => {
navigate(`?tab=${obj.tab}`)
}
return [params, setSearchParams]
}
Also, take a look at https://github.com/molefrog/wouter/pull/391
@junwen-k Hi! My guess it that
useSearchParams
from RR isn't compatible with wouter and it requires a separate implementation which is wouter-specific. I have an idea, as an experiment, use this dummy hook instead:const useSearchParams = () => { const [,navigate] = useLocation() const search = useSearch() const params = new URLSearchParams(window.location.search) const setSearchParams = (obj) => { navigate(`?tab=${obj.tab}`) } return [params, setSearchParams] }
Hi, thank you for replying. I've tested with the above snippet and the issue seems to persist. 👀 The behaviour is very buggy as you can see that sometimes it "works".
useSearchParams.ts
import { useLocation, useSearch } from "wouter";
export const useSearchParams = () => {
const [, navigate] = useLocation();
const search = useSearch();
const params = new URLSearchParams(window.location.search);
const setSearchParams = (obj) => {
navigate(`?tab=${obj.tab}`);
};
return [params, setSearchParams];
};
https://github.com/molefrog/wouter/assets/40173716/4a8bf79d-1436-45ab-8742-ff9bacd13d41
You can see that for a split second, Tab 3 works for a while, then it won't work again 🤷🏻♂️
(0:20: Refresh with ?tab=3
as default)
(0:22, 0:24: Tab 3 is selected and worked as expected?)
Right, I was able to reproduce it locally. What bothers me is that React Router also doesn't re-render when the query string changes from the outside. Try running: history.pushState(null, "", "/?tab=3")
and you will see that the tab isn't updated.
I notice that when I do
const subscribeToLocationUpdates = (callback) => {
for (const event of events) {
addEventListener(event, (...props) => {
console.log("event", event, ...props);
callback(props);
});
}
return () => {
for (const event of events) {
removeEventListener(event, callback);
}
};
};
Console actually output:
event pushState Event {isTrusted: false, arguments: Arguments(3), type: 'pushState', target: Window, currentTarget: Window, …}
Which means pushState
event is working. When I inline the arrow callback and it works as expected.
export const useSearch = ({ ssrSearch = "" } = {}) =>
useLocationProperty(
() => location.search, // Inlined
() => ssrSearch
);
May I know if this is not the right approach? I have the impression that somehow the value of location.search
gets memorised or something.
I've also noticed that react-router
discourage users to manipulate URLs using the history object directly.
⚠️ IMPORTANT For illustration only, don't use window.history.pushState directly in React Router
Not sure if running history.pushState(null, "", "/?tab=3")
not working for react-router
was due to that.
Not sure if running
history.pushState(null, "", "/?tab=3")
not working forreact-router
was due to that.
I did some digging: React Router uses its own format of state
object to perform navigation. It is not designed to be compatible with external routers, and that is completely okay as it means that wouter doesn't break it. So I narrowed down the example to only include wouter and did two experiments:
- Instead of using
useSearchParams
I useuseLocation
(as well asuseHashLocation
) to extract the current tab number from the URL, e.g./1
- Replaced that hook with a simple
useState
Interestingly, 2) works well, while 1) is still broken. It proves that the issue is indeed in wouter, but it also tells us that it's not just the search string hook.
I'll keep looking
Hey @junwen-k any update on this? I'm curious if you found out the issue.
Hey @junwen-k any update on this? I'm curious if you found out the issue.
Hi, thank you for following up. As mentioned earlier, I've actually found out a solution that worked on my end.
The changes that fixes the issue was:
- const currentSearch = () => location.search;
-
export const useSearch = ({ ssrSearch = "" } = {}) =>
- useLocationProperty(currentSearch, () => ssrSearch);
+ useLocationProperty(
+ () => location.search,
+ () => ssrSearch
+ );
I am also wondering if we should change history
as well:
- const currentHistoryState = () => history.state;
-
export const useHistoryState = () =>
- useLocationProperty(currentHistoryState, () => null);
+ useLocationProperty(
+ () => history.state,
+ () => null
+ );
However as mentioned earlier, I am not sure if this is the right approach to the issue.
I have the impression that somehow the value of location.search gets memorised or something.
Currently as a workaround, I am using my upstream branch which you can take a look here. Note that this is tightly related to https://github.com/molefrog/wouter/pull/391 as the useSearchParams
hook won't really work without this fix.
Suffering from the same bug. Would appreciate if the connected PR was merged soon.