Open illandril opened 1 year ago
I believe this is due to Next's async behavior.
You can import from next-router-mock/async
to ensure the async behavior is replicated. See: https://github.com/scottrippey/next-router-mock#sync-vs-async
Please let me know if this works for you!
Nope - the behavior is the same for both sync and async.
I think the issue is because you're creating a snapshot of the router: https://github.com/scottrippey/next-router-mock/blob/d786607dbdc04819d37f4d9eaa32bec41c2a544e/src/useMemoryRouter.tsx#L15C1-L15C86
The snapshot is intentional, because that does seem to be how Next behaves. If you've got a closure around the router
and you try to access the .query
etc, it'll give you a snapshotted (stale) value.
Here's a CodeSandbox to demonstrate:
Having said that, you're still observing a behavior that is different between next/router
and next-router-mock
, and I'd like to figure out what's going on.
Do you think you could create a CodeSandbox that demonstrates the Next behavior that you're seeing?
I forked your CodeSandbox and added a routeChangeComplete values
section that demonstrates the behavior we're relying on: https://codesandbox.io/p/sandbox/holy-platform-kk7qfj
Thanks, now I see what's happening.
In Next, it appears that the routeChangeComplete
event is triggering after React has re-rendered. Here's the order of events that I'm seeing:
useEffect
adds the routeChangeComplete
eventrouter.replace(...)
calluseRouter
hook returns a new snapshot of the router
useEffect
dependencies, causing it to unsubscribe and resubscribe the routeChangeComplete
event (with a new closure around the updated router
routeChangeComplete
event is triggered, and successfully logs the updated router
valuesSo here's where next-router-mock/async
is behaving differently. The async version adds a setTimeout(0)
before the routeChangeComplete
event, in an effort to allow React to re-render.
It's possible/probable that this isn't good enough to allow React to rerender. I wonder if a longer timeout would help?
But also, it's possible that your test isn't allowing React to rerender. Are you using React Testing Library? If so, it can be tricky to properly use act
to trigger a rerender.
For example,
await act(() => {
userEvent.click(container.getByText("Button 1"));
userEvent.click(container.getByText("Button 2"));
});
This would trigger 2 router.push
but it would only rerender once. You'd need to change that to:
await act(() => {
userEvent.click(container.getByText("Button 1"));
});
await act(() => {
userEvent.click(container.getByText("Button 2"));
});
I think it might be helpful, to move forward, if you could post some of your testing code.
There's also one more solution for you to consider. In Next, you can also use the "singleton router" for this kind of situation. The singleton router does not use snapshots, so reading the .query
will always return the latest values. For example:
import router from 'next/router';
function ExampleComponent() {
useEffect(() => {
function onRouteChange() {
// These values will always read the most up-to-date values:
console.log(router.asPath, router.query);
}
router.on('routeChangeComplete', onRouteChange);
return () => router.off('routeChangeComplete', onRouteChange);
}, [] ); // Notice, no dependencies
}
This is a much better way to subscribe to router values, because you only need useRouter
if you need the values during render. If you're only accessing router values during an event handler, then it's better to read them from the singleton.
If any of the
router
properties are accessed in a delayed fashion, if the route changes between theuseRouter
call and accessing the properties, then the original route's data is returned when usingnext-router-mock
.If the Next.js Router is used, the updated route's data is returned.
Example:
Start at
/path
, then triggerrouteChangeA
and thenrouteChangeB
. When using the Next.js Router, you'll get...... but with
next-router-mock
you'll get...