Fix context providers in SSR when handling multiple requests (#23171)
* add failing test for renderToPipeableStream * Fix context providers in SSR when handling multiple requests. Closes #23089 * Add sibling regression test Co-authored-by: zhuyi01 <zhuyi01@ke.com> Co-authored-by: Dan Abramov <dan.abramov@me.com>
Fran Dios committed
Jan 25, 2022 at 02:52 UTC
529dc3ce84f0efe99a8be33ff453c09d5801d5ca
2 files changed
+187
-2
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+184
@@ -338,4 +338,188 @@ describe('ReactDOMFizzServer', () => {
338
expect(output.result).toContain('Loading');
339
expect(isCompleteCalls).toBe(1);
340
});
341
+
342
+ // @gate experimental
343
+ it('should be able to get context value when promise resolves', async () => {
344
+ class DelayClient {
345
+ get() {
346
+ if (this.resolved) return this.resolved;
347
+ if (this.pending) return this.pending;
348
+ return (this.pending = new Promise(resolve => {
349
+ setTimeout(() => {
350
+ delete this.pending;
351
+ this.resolved = 'OK';
352
+ resolve();
353
+ }, 500);
354
+ }));
355
+ }
356
+ }
357
+
358
+ const DelayContext = React.createContext(undefined);
359
+ const Component = () => {
360
+ const client = React.useContext(DelayContext);
361
+ if (!client) {
362
+ return 'context not found.';
363
+ }
364
+ const result = client.get();
365
+ if (typeof result === 'string') {
366
+ return result;
367
+ }
368
+ throw result;
369
+ };
370
+
371
+ const client = new DelayClient();
372
+ const {writable, output, completed} = getTestWritable();
373
+ ReactDOMFizzServer.renderToPipeableStream(
374
+ <DelayContext.Provider value={client}>
375
+ <Suspense fallback="loading">
376
+ <Component />
377
+ </Suspense>
378
+ </DelayContext.Provider>,
379
+ ).pipe(writable);
380
+
381
+ jest.runAllTimers();
382
+
383
+ expect(output.error).toBe(undefined);
384
+ expect(output.result).toContain('loading');
385
+
386
+ await completed;
387
+
388
+ expect(output.error).toBe(undefined);
389
+ expect(output.result).not.toContain('context never found');
390
+ expect(output.result).toContain('OK');
391
+ });
392
+
393
+ // @gate experimental
394
+ it('should be able to get context value when calls renderToPipeableStream twice at the same time', async () => {
395
+ class DelayClient {
396
+ get() {
397
+ if (this.resolved) return this.resolved;
398
+ if (this.pending) return this.pending;
399
+ return (this.pending = new Promise(resolve => {
400
+ setTimeout(() => {
401
+ delete this.pending;
402
+ this.resolved = 'OK';
403
+ resolve();
404
+ }, 500);
405
+ }));
406
+ }
407
+ }
408
+ const DelayContext = React.createContext(undefined);
409
+ const Component = () => {
410
+ const client = React.useContext(DelayContext);
411
+ if (!client) {
412
+ return 'context never found';
413
+ }
414
+ const result = client.get();
415
+ if (typeof result === 'string') {
416
+ return result;
417
+ }
418
+ throw result;
419
+ };
420
+
421
+ const client0 = new DelayClient();
422
+ const {
423
+ writable: writable0,
424
+ output: output0,
425
+ completed: completed0,
426
+ } = getTestWritable();
427
+ ReactDOMFizzServer.renderToPipeableStream(
428
+ <DelayContext.Provider value={client0}>
429
+ <Suspense fallback="loading">
430
+ <Component />
431
+ </Suspense>
432
+ </DelayContext.Provider>,
433
+ ).pipe(writable0);
434
+
435
+ const client1 = new DelayClient();
436
+ const {
437
+ writable: writable1,
438
+ output: output1,
439
+ completed: completed1,
440
+ } = getTestWritable();
441
+ ReactDOMFizzServer.renderToPipeableStream(
442
+ <DelayContext.Provider value={client1}>
443
+ <Suspense fallback="loading">
444
+ <Component />
445
+ </Suspense>
446
+ </DelayContext.Provider>,
447
+ ).pipe(writable1);
448
+
449
+ jest.runAllTimers();
450
+
451
+ expect(output0.error).toBe(undefined);
452
+ expect(output0.result).toContain('loading');
453
+
454
+ expect(output1.error).toBe(undefined);
455
+ expect(output1.result).toContain('loading');
456
+
457
+ await Promise.all([completed0, completed1]);
458
+
459
+ expect(output0.error).toBe(undefined);
460
+ expect(output0.result).not.toContain('context never found');
461
+ expect(output0.result).toContain('OK');
462
+
463
+ expect(output1.error).toBe(undefined);
464
+ expect(output1.result).not.toContain('context never found');
465
+ expect(output1.result).toContain('OK');
466
+ });
467
+
468
+ // @gate experimental
469
+ it('should be able to pop context after suspending', async () => {
470
+ class DelayClient {
471
+ get() {
472
+ if (this.resolved) return this.resolved;
473
+ if (this.pending) return this.pending;
474
+ return (this.pending = new Promise(resolve => {
475
+ setTimeout(() => {
476
+ delete this.pending;
477
+ this.resolved = 'OK';
478
+ resolve();
479
+ }, 500);
480
+ }));
481
+ }
482
+ }
483
+
484
+ const DelayContext = React.createContext(undefined);
485
+ const Component = () => {
486
+ const client = React.useContext(DelayContext);
487
+ if (!client) {
488
+ return 'context not found.';
489
+ }
490
+ const result = client.get();
491
+ if (typeof result === 'string') {
492
+ return result;
493
+ }
494
+ throw result;
495
+ };
496
+
497
+ const client = new DelayClient();
498
+ const {writable, output, completed} = getTestWritable();
499
+ ReactDOMFizzServer.renderToPipeableStream(
500
+ <>
501
+ <DelayContext.Provider value={client}>
502
+ <Suspense fallback="loading">
503
+ <Component />
504
+ </Suspense>
505
+ </DelayContext.Provider>
506
+ <DelayContext.Provider value={client}>
507
+ <Suspense fallback="loading">
508
+ <Component />
509
+ </Suspense>
510
+ </DelayContext.Provider>
511
+ </>,
512
+ ).pipe(writable);
513
+
514
+ jest.runAllTimers();
515
+
516
+ expect(output.error).toBe(undefined);
517
+ expect(output.result).toContain('loading');
518
+
519
+ await completed;
520
+
521
+ expect(output.error).toBe(undefined);
522
+ expect(output.result).not.toContain('context never found');
523
+ expect(output.result).toContain('OK');
524
+ });
525
});
packages/react-server/src/ReactFizzNewContext.js
+3
-2
@@ -78,9 +78,10 @@ function popToNearestCommonAncestor(
78
}
79
80
popToNearestCommonAncestor(parentPrev, parentNext);
81
- // On the way back, we push the new ones that weren't common.
82
- pushNode(next);
81
}
82
+
83
+ // On the way back, we push the new ones that weren't common.
84
+ pushNode(next);
85
}
86
}
87