Open molefrog opened 6 months ago
Looks like this is the reason 🤔
It’s not recommended to suspend a render based on a store value returned by useSyncExternalStore. The reason is that mutations to the external store cannot be marked as non-blocking Transition updates, so they will trigger the nearest Suspense fallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX. https://react.dev/reference/react/useSyncExternalStore#caveats
...replacing already-rendered content on screen with a loading spinner
is exactly what is happening here
Not sure if you are accepting new code, based on the last open PRs
Not sure if you are accepting new code, based on the last open PRs
We are accepting new code! The current open PRs are quite difficult to merge, since there were intended for v2, but I stopped adding new features when I was working on v3 release. I think these PRs have to be closed unfortunately and reimplemented from scratch.
Your contributions are always welcome!
Oh cool! I'll work on a fix this weekend :D Thanks @molefrog!
Hi! I created this as a "hotfix" locally - just works™️
import { IS_BROWSER } from 'powership';
function parseLocationEntries() {
return [
['path', window.location.pathname],
['search', window.location.search],
] as const;
}
export type LocationEntry = ReturnType<typeof parseLocationEntries>;
export type RouteChangeCallback = (changes: LocationEntry) => void;
const subscribers = new Set<RouteChangeCallback>();
if (IS_BROWSER) {
let current = parseLocationEntries();
const callback = () => {
if (!subscribers.size) return;
const next = parseLocationEntries();
const changed = next.filter((el, idx) => el[1] !== current[idx][1]);
if (!changed.length) return;
current = next;
subscribers.forEach((fn) => {
fn(current);
});
};
const events = ['popstate', 'pushState', 'replaceState', 'hashchange'];
for (const event of events) {
addEventListener(event, callback);
}
}
export function observeRouteChange(cb: RouteChangeCallback) {
subscribers.add(cb);
return () => {
subscribers.delete(cb);
};
}
// example
function useRoute() {
const router = useWouter();
const [route, setRoute] = React.useState(() => {
return {
search: router.ssrSearch || '',
path: router.ssrPath || '',
};
});
useEffect(() => {
return observeRouteChange((changes) => {
setRoute((state) => {
changes.forEach(([k, v]) => {
state = { ...state, [k]: v };
});
return state;
});
});
}, []);
return route;
}
edit: removed startTransition
How to reproduce:
<Router ssrPath={requestUrl.pathname} ssrSearch={requestUrl.search}>
<Router />
Expected: when wouter runs in the browser, by default it should fill in
ssrPath
andssrSearch
with current pathname and search string extracted fromlocation