molefrog / wouter

🥢 A minimalist-friendly ~2.1KB routing for React and Preact
https://npm.im/wouter
The Unlicense
6.65k stars 152 forks source link

Potential `useSearch` bug not triggering updates when used with `urql` #393

Open junwen-k opened 10 months ago

junwen-k commented 10 months ago

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

  1. Clone https://github.com/junwen-k/react-router-wouter

    git clone https://github.com/junwen-k/react-router-wouter
    cd react-router-wouter
  2. Install dependencies

    npm install
  3. Start Server

    npm start
  4. 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.

junwen-k commented 10 months ago

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!

molefrog commented 10 months ago

@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]
}
molefrog commented 10 months ago

Also, take a look at https://github.com/molefrog/wouter/pull/391

junwen-k commented 9 months ago

@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?)

molefrog commented 9 months ago

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.

junwen-k commented 9 months ago

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.

molefrog commented 9 months ago

Not sure if running history.pushState(null, "", "/?tab=3") not working for react-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:

  1. Instead of using useSearchParams I use useLocation (as well as useHashLocation) to extract the current tab number from the URL, e.g. /1
  2. 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

molefrog commented 7 months ago

Hey @junwen-k any update on this? I'm curious if you found out the issue.

junwen-k commented 7 months ago

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.

pReya commented 5 months ago

Suffering from the same bug. Would appreciate if the connected PR was merged soon.