Skip to content

Commit eea98f4

Browse files
fix: Ensure same-component routes are rerendered rather than swapped (#16)
* fix: Avoid swapping routes if component hasn't changed Co-authored-by: Jason Miller <[email protected]> * fix: Reset routeChanged * test: Add test impl --------- Co-authored-by: Jason Miller <[email protected]>
1 parent 22df871 commit eea98f4

File tree

2 files changed

+78
-10
lines changed

2 files changed

+78
-10
lines changed

src/router.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,24 +117,33 @@ export function Router(props) {
117117
// was the most recent render successful (did not suspend):
118118
const didSuspend = useRef();
119119
didSuspend.current = false;
120-
121-
useMemo(() => {
122-
// This hack prevents Preact from diffing when we swap `cur` to `prev`:
123-
if (this.__v && this.__v.__k) this.__v.__k.reverse();
124-
125-
count.current++;
126-
prev.current = cur.current;
127-
}, [url]);
120+
// has the route component changed
121+
const routeChanged = useRef(false);
128122

129123
let pr, d, m;
130124
toChildArray(props.children).some(vnode => {
131125
const matches = exec(rest, vnode.props.path, (m = { ...vnode.props, path: rest, query, params, rest: '' }));
132126
if (matches) return (pr = cloneElement(vnode, m));
133127
if (vnode.props.default) d = cloneElement(vnode, m);
134128
});
129+
130+
let incoming = pr || d;
131+
useMemo(() => {
132+
prev.current = cur.current;
133+
134+
// Only mark as an update if the route component changed.
135+
const outgoing = prev.current && prev.current.props.children;
136+
if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) {
137+
// This hack prevents Preact from diffing when we swap `cur` to `prev`:
138+
if (this.__v && this.__v.__k) this.__v.__k.reverse();
139+
count.current++;
140+
routeChanged.current = true;
141+
} else routeChanged.current = false;
142+
}, [url]);
143+
135144
const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;
136145
const isHydratingBool = cur.current && cur.current.__h;
137-
cur.current = h(RouteContext.Provider, { value: m }, pr || d);
146+
cur.current = h(RouteContext.Provider, { value: m }, incoming);
138147
if (isHydratingSuspense) {
139148
cur.current.__u |= MODE_HYDRATE;
140149
cur.current.__u |= MODE_SUSPENDED;
@@ -220,7 +229,9 @@ export function Router(props) {
220229
}, [path, wasPush, c]);
221230

222231
// Note: curChildren MUST render first in order to set didSuspend & prev.
223-
return [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })];
232+
return routeChanged.current
233+
? [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })]
234+
: h(RenderRef, { r: cur });
224235
}
225236

226237
const MODE_HYDRATE = 1 << 5;

test/router.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,63 @@ describe('Router', () => {
306306
expect(A).toHaveBeenCalledWith({ path: '/', query: {}, params: {}, rest: '' }, expect.anything());
307307
});
308308

309+
it('rerenders same-component routes rather than swap', async () => {
310+
const A = jest.fn(groggy(() => html`<h1>a</h1>`, 1));
311+
const B = jest.fn(groggy(({ sub }) => html`<h1>b/${sub}</h1>`, 1));
312+
let loc, childrenLength;
313+
314+
const old = options.__c;
315+
options.__c = (vnode, queue) => {
316+
if (vnode.type === Router) {
317+
childrenLength = vnode.__k.length;
318+
}
319+
320+
if (old) old(vnode, queue);
321+
}
322+
323+
render(
324+
html`
325+
<${ErrorBoundary}>
326+
<${LocationProvider}>
327+
<${Router}>
328+
<${A} path="/" />
329+
<${B} path="/b/:sub" />
330+
<//>
331+
<${() => {
332+
loc = useLocation();
333+
}} />
334+
<//>
335+
<//>
336+
`,
337+
scratch
338+
);
339+
340+
await sleep(10);
341+
342+
expect(scratch).toHaveProperty('innerHTML', '<h1>a</h1>');
343+
expect(childrenLength).toBe(2);
344+
345+
loc.route('/b/a');
346+
await sleep(10);
347+
348+
expect(scratch).toHaveProperty('innerHTML', '<h1>b/a</h1>');
349+
expect(childrenLength).toBe(2);
350+
351+
loc.route('/b/b');
352+
await sleep(10);
353+
354+
expect(scratch).toHaveProperty('innerHTML', '<h1>b/b</h1>');
355+
expect(childrenLength).toBe(1);
356+
357+
loc.route('/');
358+
await sleep(10);
359+
360+
expect(scratch).toHaveProperty('innerHTML', '<h1>a</h1>');
361+
expect(childrenLength).toBe(2);
362+
363+
options.__c = old;
364+
});
365+
309366
it('should support onLoadStart/onLoadEnd/onRouteChange w/out navigation', async () => {
310367
const route = name => html`
311368
<h1>${name}</h1>

0 commit comments

Comments
 (0)