facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.61k stars 1.19k forks source link

`urlSyncEffect` not syncing after an external url change #1975

Open nichita-pasecinic opened 2 years ago

nichita-pasecinic commented 2 years ago

If using urlSyncEffect effect I assume that any mutation to recoil atom would change the url (In may case just a query param key), but as well in case of a navigation from client side with a link, or useNavigate would update the atom value

const navigate = useNavigate();
navigate('/something'); // <-- removes the current query params (linked with recoil atom), but does not reset them 
nichita-pasecinic commented 2 years ago

Are there any ways I could remove the queryParam from the url bar and have the atom being reset to the default state ?

drarmstr commented 2 years ago

Recoil Sync attempts to sync bidirectionally, so changing an atom will update the URL and changing the URL will update the atom. (Note #1900 which may also be related to your issue, though that is fixable) It relies on subscribing to the browser popstate event for URL changes. This covered the usecase of using the Browser "back" button which was our priority. However, it appears this event is not triggered with all forms of navigation. There are also a hashchange event, but I think someone tried testing with that without much luck. I'm going to assume by useNavigate() you are referencing React Router. I don't know their internals, but assume they are using history.replace(); or history.pushstate();. As a workaround some have either manually triggered the popstate event or make extra history.pushstate(); history.popstate(); calls. Another solution might be to update the URL sync logic with polling, if someone wanted to add that.

salvoravida commented 2 years ago

@drarmstr https://github.com/remix-run/history/blob/dev/packages/history/index.ts

Maybe recoil URL sync could use well-tested history wrapper: npm "history" package (from the team of react-router)

drarmstr commented 2 years ago

@salvoravida Looking at the source of history package it looks like they also just rely on the popstate and hashchange events. I can add the hashchange event for more coverage. Though, I did actually manually test out some external URL changes and found even with just popstate it was working with relative links with href that changed either the search params or anchor hash and a button that changed location.hash with Chrome at least. Not sure what mechanism useNavigate() uses.

https://codesandbox.io/s/heuristic-elion-suen5c?file=/src/App.tsx

lucasavila00 commented 1 year ago

A workaround for react-router is to set a custom BrowserInterface:

const RecoilURLSyncJSONConfig: LocationOption = { part: "queryParams" };

<RecoilRoot>
    <RecoilURLSyncJSON
        location={RecoilURLSyncJSONConfig}
        browserInterface={{
            replaceURL: url => history.replace(url.replace(window.location.origin, "")),
            pushURL: url => history.push(url.replace(window.location.origin, "")),
            getURL: () =>
                window.location.origin +
                history.location.pathname +
                history.location.search +
                history.location.hash,
            listenChangeURL: handler => {
                const close = history.listen((_location, _action) => {
                    handler();
                });
                return () => {
                    close();
                };
            },
        }}
    >
        <Router history={history}>
....