Skip to content

Commit ccff8da

Browse files
authored
Fixed incomplete SSR hydration (#22)
1 parent 16e13fc commit ccff8da

8 files changed

+84
-25
lines changed

src/main/enableSSRHydration.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ export function enableSSRHydration(executorManager: ExecutorManager, options: SS
3636
}
3737

3838
window.__REACT_EXECUTOR_SSR_STATE__ = {
39-
push(chunk) {
40-
executorManager.hydrate(stateParser(chunk));
39+
push() {
40+
for (let i = 0; i < arguments.length; ++i) {
41+
executorManager.hydrate(stateParser(arguments[i]));
42+
}
4143
},
4244
};
4345
}

src/main/global.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ declare global {
44
const __REACT_EXECUTOR_DEVTOOLS__: { plugin: ExecutorPlugin } | undefined;
55

66
interface Window {
7-
__REACT_EXECUTOR_SSR_STATE__?: { push(stateStr: string): void };
7+
__REACT_EXECUTOR_SSR_STATE__?: { push(...stateStrs: string[]): void };
88
}
99
}

src/main/ssr/ReadableSSRExecutorManager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class ReadableSSRExecutorManager extends SSRExecutorManager implements Re
2323

2424
const hydrationChunk = this.nextHydrationChunk();
2525

26-
if (hydrationChunk !== undefined) {
26+
if (hydrationChunk !== '') {
2727
controller.enqueue(hydrationChunk);
2828
}
2929
},

src/main/ssr/SSRExecutorManager.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,29 @@ export class SSRExecutorManager extends ExecutorManager {
5858
}
5959

6060
/**
61-
* Returns a chunk that hydrates the client with the state accumulated during SSR, or `undefined` if there are no
61+
* Returns a chunk that hydrates the client with the state accumulated during SSR, or an empty string if there are no
6262
* state changes since the last time {@link nextHydrationChunk} was called.
6363
*/
64-
nextHydrationChunk(): string | undefined {
65-
const sources = [];
64+
nextHydrationChunk(): string {
65+
const stateStrs = [];
6666

6767
for (const executor of this._executors.values()) {
6868
const hydratedVersion = this._hydratedVersions.get(executor);
6969

7070
if ((hydratedVersion === undefined || hydratedVersion !== executor.version) && this._executorFilter(executor)) {
71-
sources.push(JSON.stringify(this._stateStringifier(executor.toJSON())));
71+
stateStrs.push(JSON.stringify(this._stateStringifier(executor.toJSON())));
7272

7373
this._hydratedVersions.set(executor, executor.version);
7474
}
7575
}
7676

77-
if (sources.length === 0) {
78-
return;
77+
if (stateStrs.length === 0) {
78+
return '';
7979
}
8080

8181
return (
8282
'<script>(window.__REACT_EXECUTOR_SSR_STATE__=window.__REACT_EXECUTOR_SSR_STATE__||[]).push(' +
83-
sources.join(',') +
83+
stateStrs.join(',') +
8484
');var e=document.currentScript;e&&e.parentNode.removeChild(e)</script>'
8585
);
8686
}

src/main/ssr/node/PipeableSSRExecutorManager.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ export class PipeableSSRExecutorManager extends SSRExecutorManager {
2929

3030
const hydrationChunk = this.nextHydrationChunk();
3131

32-
if (hydrationChunk !== undefined) {
32+
if (hydrationChunk !== '') {
3333
stream.write(hydrationChunk, callback);
34-
} else {
35-
callback();
34+
return;
3635
}
36+
37+
callback();
3738
});
3839
},
3940

src/main/types.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,18 @@ export interface ExecutorEvent<Value = any> {
8080
* See {@link ExecutorEvent} for more details.
8181
*/
8282
type:
83-
| 'plugin_configured'
8483
| 'attached'
84+
| 'detached'
8585
| 'activated'
86-
| 'annotated'
86+
| 'deactivated'
8787
| 'pending'
8888
| 'fulfilled'
8989
| 'rejected'
9090
| 'aborted'
9191
| 'cleared'
9292
| 'invalidated'
93-
| 'deactivated'
94-
| 'detached'
93+
| 'annotated'
94+
| 'plugin_configured'
9595
| (string & {});
9696

9797
/**

src/test/enableSSRHydration.test.ts

+35
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,41 @@ describe('enableSSRHydration', () => {
2828
expect(executor.settledAt).toBe(50);
2929
});
3030

31+
test('hydrates multiple executors that are added after', () => {
32+
const manager = new ExecutorManager();
33+
34+
enableSSRHydration(manager);
35+
36+
window.__REACT_EXECUTOR_SSR_STATE__!.push(
37+
JSON.stringify({
38+
key: 'xxx',
39+
isFulfilled: true,
40+
value: 111,
41+
reason: undefined,
42+
settledAt: 50,
43+
invalidatedAt: 0,
44+
annotations: {},
45+
}),
46+
JSON.stringify({
47+
key: 'yyy',
48+
isFulfilled: true,
49+
value: 222,
50+
reason: undefined,
51+
settledAt: 100,
52+
invalidatedAt: 0,
53+
annotations: {},
54+
})
55+
);
56+
57+
const executor1 = manager.getOrCreate('xxx');
58+
const executor2 = manager.getOrCreate('yyy');
59+
60+
expect(executor1.value).toBe(111);
61+
expect(executor1.settledAt).toBe(50);
62+
expect(executor2.value).toBe(222);
63+
expect(executor2.settledAt).toBe(100);
64+
});
65+
3166
test('hydrates an executor that was added before', () => {
3267
window.__REACT_EXECUTOR_SSR_STATE__ = [
3368
JSON.stringify({

src/test/ssr/SSRExecutorManager.test.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ describe('SSRExecutorManager', () => {
1010

1111
manager.getOrCreate('xxx');
1212

13-
expect(manager.nextHydrationChunk()).toBeUndefined();
13+
expect(manager.nextHydrationChunk()).toBe('');
1414
});
1515

16-
test('returns a hydration chunk', async () => {
16+
test('returns the hydration chunk for a single executor', async () => {
1717
const manager = new SSRExecutorManager();
1818

1919
const executor = manager.getOrCreate('xxx');
2020

21-
expect(manager.nextHydrationChunk()).toBeUndefined();
21+
expect(manager.nextHydrationChunk()).toBe('');
2222

2323
const promise = executor.execute(() => 111);
2424

25-
expect(manager.nextHydrationChunk()).toBeUndefined();
25+
expect(manager.nextHydrationChunk()).toBe('');
2626

2727
await promise;
2828

@@ -31,6 +31,27 @@ describe('SSRExecutorManager', () => {
3131
);
3232
});
3333

34+
test('returns the hydration chunk for multiple executors', async () => {
35+
const manager = new SSRExecutorManager();
36+
37+
const executor1 = manager.getOrCreate('xxx');
38+
const executor2 = manager.getOrCreate('yyy');
39+
40+
expect(manager.nextHydrationChunk()).toBe('');
41+
42+
const promise1 = executor1.execute(() => 111);
43+
const promise2 = executor2.execute(() => 222);
44+
45+
expect(manager.nextHydrationChunk()).toBe('');
46+
47+
await promise1;
48+
await promise2;
49+
50+
expect(manager.nextHydrationChunk()).toBe(
51+
'<script>(window.__REACT_EXECUTOR_SSR_STATE__=window.__REACT_EXECUTOR_SSR_STATE__||[]).push("{\\"key\\":\\"xxx\\",\\"isFulfilled\\":true,\\"value\\":111,\\"annotations\\":{},\\"settledAt\\":50,\\"invalidatedAt\\":0}","{\\"key\\":\\"yyy\\",\\"isFulfilled\\":true,\\"value\\":222,\\"annotations\\":{},\\"settledAt\\":50,\\"invalidatedAt\\":0}");var e=document.currentScript;e&&e.parentNode.removeChild(e)</script>'
52+
);
53+
});
54+
3455
test('returns only changed executor states in consequent hydration chunks', async () => {
3556
const manager = new SSRExecutorManager();
3657

@@ -43,7 +64,7 @@ describe('SSRExecutorManager', () => {
4364

4465
const promise = executor2.execute(() => 222);
4566

46-
expect(manager.nextHydrationChunk()).toBeUndefined();
67+
expect(manager.nextHydrationChunk()).toBe('');
4768

4869
await promise;
4970

@@ -60,7 +81,7 @@ describe('SSRExecutorManager', () => {
6081
.execute(() => Promise.reject('expected'))
6182
.catch(noop);
6283

63-
expect(manager.nextHydrationChunk()).toBeUndefined();
84+
expect(manager.nextHydrationChunk()).toBe('');
6485
});
6586

6687
test('respects executorFilter option', async () => {
@@ -78,7 +99,7 @@ describe('SSRExecutorManager', () => {
7899
);
79100
});
80101

81-
test('respects executorFilter option', async () => {
102+
test('respects stateStringifier option', async () => {
82103
const stateStringifierMock = jest.fn(JSON.stringify);
83104

84105
const manager = new SSRExecutorManager({

0 commit comments

Comments
 (0)