Skip to content

Commit 5441f5f

Browse files
authored
Merge pull request #20 from preactjs/ensure-we-carry-over-suspense
ensure we carry over suspense
2 parents bf71693 + 1af5a7d commit 5441f5f

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

src/router.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,22 @@ export function Router(props) {
132132
if (matches) return (pr = cloneElement(vnode, m));
133133
if (vnode.props.default) d = cloneElement(vnode, m);
134134
});
135+
const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED;
136+
const isHydratingBool = cur.current && cur.current.__h;
135137
cur.current = h(RouteContext.Provider, { value: m }, pr || d);
138+
if (isHydratingSuspense) {
139+
cur.current.__u |= MODE_HYDRATE;
140+
cur.current.__u |= MODE_SUSPENDED;
141+
} else if (isHydratingBool) {
142+
cur.current.__h = true;
143+
}
136144

137145
// Reset previous children - if rendering succeeds synchronously, we shouldn't render the previous children.
138146
const p = prev.current;
139147
prev.current = null;
140148

141149
// This borrows the _childDidSuspend() solution from compat.
142-
this.__c = e => {
150+
this.__c = (e, suspendedVNode) => {
143151
// Mark the current render as having suspended:
144152
didSuspend.current = true;
145153

@@ -158,6 +166,22 @@ export function Router(props) {
158166

159167
// Successful route transition: un-suspend after a tick and stop rendering the old route:
160168
prev.current = null;
169+
if (cur.current) {
170+
if (suspendedVNode.__h) {
171+
// _hydrating
172+
cur.current.__h = suspendedVNode.__h;
173+
}
174+
175+
if (suspendedVNode.__u & MODE_SUSPENDED) {
176+
// _flags
177+
cur.current.__u |= MODE_SUSPENDED;
178+
}
179+
180+
if (suspendedVNode.__u & MODE_HYDRATE) {
181+
cur.current.__u |= MODE_HYDRATE;
182+
}
183+
}
184+
161185
RESOLVED.then(update);
162186
});
163187
};
@@ -199,6 +223,9 @@ export function Router(props) {
199223
return [h(RenderRef, { r: cur }), h(RenderRef, { r: prev })];
200224
}
201225

226+
const MODE_HYDRATE = 1 << 5;
227+
const MODE_SUSPENDED = 1 << 7;
228+
202229
// Lazily render a ref's current value:
203230
const RenderRef = ({ r }) => r.current;
204231

test/router.test.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { jest, describe, it, beforeEach, expect } from '@jest/globals';
2-
import { h, render } from 'preact';
2+
import { h, hydrate, options, render } from 'preact';
33
import { useState } from 'preact/hooks';
44
import { html } from 'htm/preact';
55
import { LocationProvider, Router, useLocation, Route, useRoute } from '../src/router.js';
@@ -760,3 +760,82 @@ describe('Router', () => {
760760
replaceState.mockRestore();
761761
});
762762
});
763+
764+
describe('hydration', () => {
765+
let scratch;
766+
beforeEach(() => {
767+
if (scratch) {
768+
render(null, scratch);
769+
scratch.remove();
770+
}
771+
scratch = document.createElement('scratch');
772+
document.body.appendChild(scratch);
773+
history.replaceState(null, null, '/');
774+
});
775+
776+
it.only('should wait for asynchronous routes', async () => {
777+
scratch.innerHTML = '<div><h1>A</h1><p>hello</p></div>';
778+
const route = name => html`
779+
<div>
780+
<h1>${name}</h1>
781+
<p>hello</p>
782+
</div>
783+
`;
784+
const A = jest.fn(groggy(() => route('A'), 1));
785+
let loc;
786+
hydrate(
787+
html`
788+
<${ErrorBoundary}>
789+
<${LocationProvider}>
790+
<${Router}>
791+
<${A} path="/" />
792+
<//>
793+
<${() => {
794+
loc = useLocation();
795+
}} />
796+
<//>
797+
<//>
798+
`,
799+
scratch
800+
);
801+
802+
const mutations = [];
803+
const mutationObserver = new MutationObserver((x) => {
804+
mutations.push(...x)
805+
});
806+
mutationObserver.observe(scratch, { childList: true, subtree: true });
807+
808+
expect(scratch).toHaveProperty('innerHTML', '<div><h1>A</h1><p>hello</p></div>');
809+
expect(A).toHaveBeenCalledWith({ path: '/', query: {}, params: {}, rest: '' }, expect.anything());
810+
const oldOptionsVnode = options.__b;
811+
let hasMatched = false;
812+
options.__b = (vnode) => {
813+
if (vnode.type === A && !hasMatched) {
814+
hasMatched = true;
815+
if (vnode.__ && vnode.__.__h) {
816+
expect(vnode.__.__h).toBe(true)
817+
} else if (vnode.__ && vnode.__.__u) {
818+
expect(!!(vnode.__.__u & MODE_SUSPENDED)).toBe(true);
819+
expect(!!(vnode.__.__u & MODE_HYDRATE)).toBe(true);
820+
} else {
821+
expect(true).toBe(false);
822+
}
823+
}
824+
825+
if (oldOptionsVnode) {
826+
oldOptionsVnode(vnode);
827+
}
828+
}
829+
A.mockClear();
830+
await sleep(10);
831+
832+
expect(scratch).toHaveProperty('innerHTML', '<div><h1>A</h1><p>hello</p></div>');
833+
expect(A).toHaveBeenCalledWith({ path: '/', query: {}, params: {}, rest: '' }, expect.anything());
834+
expect(mutations).toHaveLength(0);
835+
836+
options.__b = oldOptionsVnode;
837+
});
838+
})
839+
840+
const MODE_HYDRATE = 1 << 5;
841+
const MODE_SUSPENDED = 1 << 7;

0 commit comments

Comments
 (0)