Skip to content

Commit 58f1bff

Browse files
authored
Bugfix:router hydration duplication (#364)
* Fix duplicated suspense content during hydration * Update demo's copy of the router (oops) * Create silly-shrimps-build.md
1 parent aa7ffb3 commit 58f1bff

File tree

3 files changed

+40
-28
lines changed

3 files changed

+40
-28
lines changed

.changeset/silly-shrimps-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"preact-iso": patch
3+
---
4+
5+
Fixes a bug introduced in 1.0.0 where Router would duplicate DOM when hydrating `lazy()` components.

packages/preact-iso/router.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export function Router(props) {
7777
const prevChildren = useRef();
7878
const pending = useRef();
7979

80+
let reverse = false;
8081
if (url !== cur.current.url) {
82+
reverse = true;
8183
pending.current = null;
8284
prev.current = cur.current;
8385
prevChildren.current = curChildren.current;
@@ -119,7 +121,7 @@ export function Router(props) {
119121

120122
// Hi! Wondering what this horrid line is for? That's totally reasonable, it is gross.
121123
// It prevents the old route from being remounted because it got shifted in the children Array.
122-
if (this.__v && this.__v.__k) this.__v.__k.reverse();
124+
if (reverse && this.__v && this.__v.__k) this.__v.__k.reverse();
123125

124126
return [curChildren.current, prevChildren.current];
125127
}

packages/wmr/demo/public/lib/loc.js

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { h, createContext, cloneElement } from 'preact';
2-
import { useContext, useMemo, useReducer, useEffect, useRef } from 'preact/hooks';
2+
import { useContext, useMemo, useReducer, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
33

44
const UPDATE = (state, url, push) => {
55
if (url && url.type === 'click') {
@@ -18,11 +18,11 @@ const UPDATE = (state, url, push) => {
1818
return url;
1919
};
2020

21-
const exec = (url, route, matches) => {
21+
export const exec = (url, route, matches) => {
2222
url = url.trim('/').split('/');
2323
route = (route || '').trim('/').split('/');
2424
for (let i = 0, val; i < Math.max(url.length, route.length); i++) {
25-
let [, m, param, flag] = (route[i] || '').match(/^(\:?)(.*?)([+*?]?)$/);
25+
let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
2626
val = url[i];
2727
// segment match:
2828
if (!m && param == val) continue;
@@ -58,7 +58,7 @@ export function LocationProvider(props) {
5858
removeEventListener('click', route);
5959
removeEventListener('popstate', route);
6060
};
61-
});
61+
}, []);
6262

6363
// @ts-ignore
6464
return h(LocationProvider.ctx.Provider, { value }, props.children);
@@ -77,27 +77,39 @@ export function Router(props) {
7777
const prevChildren = useRef();
7878
const pending = useRef();
7979

80+
let reverse = false;
8081
if (url !== cur.current.url) {
82+
reverse = true;
8183
pending.current = null;
8284
prev.current = cur.current;
8385
prevChildren.current = curChildren.current;
86+
// old <Committer> uses the pending promise ref to know whether to render
87+
prevChildren.current.props.pending = pending;
8488
cur.current = loc;
8589
}
8690

91+
curChildren.current = useMemo(() => {
92+
let p, d, m;
93+
[].concat(props.children || []).some(vnode => {
94+
const matches = exec(path, vnode.props.path, (m = { path, query }));
95+
if (matches) return (p = cloneElement(vnode, m));
96+
if (vnode.props.default) d = cloneElement(vnode, m);
97+
});
98+
99+
return h(Committer, {}, h(RouteContext.Provider, { value: m }, p || d));
100+
}, [url]);
101+
87102
this.componentDidCatch = err => {
88-
if (err && err.then) {
89-
// Trigger an update so the rendering login will pickup the pending promise.
90-
update(1);
91-
pending.current = err;
92-
}
103+
if (err && err.then) pending.current = err;
93104
};
94105

95-
useEffect(() => {
106+
useLayoutEffect(() => {
96107
let p = pending.current;
108+
97109
const commit = () => {
98110
if (cur.current.url !== url || pending.current !== p) return;
111+
prev.current = prevChildren.current = pending.current = null;
99112
if (props.onLoadEnd) props.onLoadEnd(url);
100-
prev.current = prevChildren.current = null;
101113
update(0);
102114
};
103115

@@ -107,27 +119,20 @@ export function Router(props) {
107119
} else commit();
108120
}, [url]);
109121

110-
let p, d, m;
111-
[].concat(props.children || []).some(vnode => {
112-
const matches = exec(path, vnode.props.path, (m = { path, query }));
113-
if (matches) {
114-
return (p = (
115-
<RouteContext.Provider value={{ ...matches }}>
116-
{cloneElement(vnode, { ...m, ...matches })}
117-
</RouteContext.Provider>
118-
));
119-
}
120-
if (vnode.props.default) d = cloneElement(vnode, m);
121-
return undefined;
122-
});
123-
124-
return [(curChildren.current = p || d), prevChildren.current];
122+
// Hi! Wondering what this horrid line is for? That's totally reasonable, it is gross.
123+
// It prevents the old route from being remounted because it got shifted in the children Array.
124+
if (reverse && this.__v && this.__v.__k) this.__v.__k.reverse();
125+
126+
return [curChildren.current, prevChildren.current];
127+
}
128+
129+
function Committer({ pending, children }) {
130+
return pending && !pending.current ? null : children;
125131
}
126132

127133
Router.Provider = LocationProvider;
128134

129135
LocationProvider.ctx = createContext(/** @type {{ url: string, path: string, query: object, route }} */ ({}));
130-
131136
const RouteContext = createContext({});
132137

133138
export const useLocation = () => useContext(LocationProvider.ctx);

0 commit comments

Comments
 (0)