Skip to content

Commit e23673b

Browse files
authored
[Flight] Add getCacheForType() to the dispatcher (#20315)
* Remove react/unstable_cache We're probably going to make it available via the dispatcher. Let's remove this for now. * Add readContext() to the dispatcher On the server, it will be per-request. On the client, there will be some way to shadow it. For now, I provide it on the server, and throw on the client. * Use readContext() from react-fetch This makes it work on the server (but not on the client until we implement it there.) Updated the test to use Server Components. Now it passes. * Fixture: Add fetch from a Server Component * readCache -> getCacheForType<T> * Add React.unstable_getCacheForType * Add a feature flag * Fix Flow * Add react-suspense-test-utils and port tests * Remove extra Map lookup * Unroll async/await because build system * Add some error coverage and retry * Add unstable_getCacheForType to Flight entry
1 parent 555eeae commit e23673b

37 files changed

Lines changed: 363 additions & 156 deletions

‎fixtures/flight/server/cli.server.js‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,28 @@ const app = express();
1717
// Application
1818
app.get('/',function(req,res){
1919
if(process.env.NODE_ENV==='development'){
20-
for(varkeyinrequire.cache){
21-
deleterequire.cache[key];
22-
}
20+
// This doesn't work in ESM mode.
21+
// for (var key in require.cache) {
22+
// delete require.cache[key];
23+
// }
2324
}
2425
require('./handler.server.js')(req,res);
2526
});
2627

28+
app.get('/todos',function(req,res){
29+
res.setHeader('Access-Control-Allow-Origin','*');
30+
res.json([
31+
{
32+
id: 1,
33+
text: 'Shave yaks',
34+
},
35+
{
36+
id: 2,
37+
text: 'Eat kale',
38+
},
39+
]);
40+
});
41+
2742
app.listen(3001,()=>{
2843
console.log('Flight Server listening on port 3001...');
2944
});

‎fixtures/flight/src/App.server.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import*asReactfrom'react';
2+
import{fetch}from'react-fetch';
23

34
importContainerfrom'./Container.js';
45

@@ -8,11 +9,17 @@ import {Counter as Counter2} from './Counter2.client.js';
89
importShowMorefrom'./ShowMore.client.js';
910

1011
exportdefaultfunctionApp(){
12+
consttodos=fetch('http://localhost:3001/todos').json();
1113
return(
1214
<Container>
1315
<h1>Hello, world</h1>
1416
<Counter/>
1517
<Counter2/>
18+
<ul>
19+
{todos.map(todo=>(
20+
<likey={todo.id}>{todo.text}</li>
21+
))}
22+
</ul>
1623
<ShowMore>
1724
<p>Lorem ipsum</p>
1825
</ShowMore>

‎packages/react-debug-tools/src/ReactDebugHooks.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
2323
import{NoMode}from'react-reconciler/src/ReactTypeOfMode';
2424

2525
importErrorStackParserfrom'error-stack-parser';
26+
importinvariantfrom'shared/invariant';
2627
importReactSharedInternalsfrom'shared/ReactSharedInternals';
2728
import{REACT_OPAQUE_ID_TYPE}from'shared/ReactSymbols';
2829
import{
@@ -100,6 +101,10 @@ function nextHook(): null | Hook {
100101
returnhook;
101102
}
102103

104+
functiongetCacheForType<T>(resourceType: () => T): T{
105+
invariant(false,'Not implemented.');
106+
}
107+
103108
functionreadContext<T>(
104109
context: ReactContext<T>,
105110
observedBits: void | number | boolean,
@@ -298,6 +303,7 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
298303
}
299304

300305
constDispatcher: DispatcherType={
306+
getCacheForType,
301307
readContext,
302308
useCallback,
303309
useContext,

‎packages/react-dom/src/server/ReactPartialRendererHooks.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type PartialRenderer from './ReactPartialRenderer';
2020
import{validateContextBounds}from'./ReactPartialRendererContext';
2121

2222
importinvariantfrom'shared/invariant';
23+
import{enableCache}from'shared/ReactFeatureFlags';
2324
importisfrom'shared/objectIs';
2425

2526
typeBasicStateAction<S>=(S=>S)|S;
@@ -214,6 +215,10 @@ export function resetHooksState(): void {
214215
workInProgressHook=null;
215216
}
216217

218+
functiongetCacheForType<T>(resourceType: () => T): T{
219+
invariant(false,'Not implemented.');
220+
}
221+
217222
functionreadContext<T>(
218223
context: ReactContext<T>,
219224
observedBits: void | number | boolean,
@@ -512,3 +517,7 @@ export const Dispatcher: DispatcherType = {
512517
// Subscriptions are not setup in a server environment.
513518
useMutableSource,
514519
};
520+
521+
if (enableCache) {
522+
Dispatcher.getCacheForType=getCacheForType;
523+
}

‎packages/react-fetch/src/ReactFetchBrowser.js‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
importtype{Wakeable}from'shared/ReactTypes';
1111

12-
import{readCache}from'react/unstable-cache';
12+
import{unstable_getCacheForType}from'react';
1313

1414
constPending=0;
1515
constResolved=1;
@@ -34,16 +34,13 @@ type Result = PendingResult | ResolvedResult | RejectedResult;
3434

3535
// TODO: this is a browser-only version. Add a separate Node entry point.
3636
constnativeFetch=window.fetch;
37-
constfetchKey={};
38-
39-
functionreadResultMap(): Map<string,Result>{
40-
constresources=readCache().resources;
41-
letmap=resources.get(fetchKey);
42-
if(map===undefined){
43-
map=newMap();
44-
resources.set(fetchKey,map);
45-
}
46-
returnmap;
37+
38+
functiongetResultMap(): Map<string,Result>{
39+
returnunstable_getCacheForType(createResultMap);
40+
}
41+
42+
functioncreateResultMap(): Map<string,Result>{
43+
returnnewMap();
4744
}
4845

4946
functiontoResult(thenable): Result{
@@ -120,7 +117,7 @@ Response.prototype = {
120117
};
121118

122119
functionpreloadResult(url: string,options: mixed): Result{
123-
constmap=readResultMap();
120+
constmap=getResultMap();
124121
letentry=map.get(url);
125122
if(!entry){
126123
if(options){

‎packages/react-fetch/src/ReactFetchNode.js‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes';
1111

1212
import*ashttpfrom'http';
1313
import*ashttpsfrom'https';
14-
15-
import{readCache}from'react/unstable-cache';
14+
import{unstable_getCacheForType}from'react';
1615

1716
typeFetchResponse={|
1817
// Properties
@@ -75,16 +74,12 @@ type RejectedResult = {|
7574

7675
typeResult<V>=PendingResult|ResolvedResult<V>|RejectedResult;
7776

78-
constfetchKey={};
77+
functiongetResultMap(): Map<string,Result<FetchResponse>>{
78+
returnunstable_getCacheForType(createResultMap);
79+
}
7980

80-
functionreadResultMap(): Map<string,Result<FetchResponse>>{
81-
constresources=readCache().resources;
82-
letmap=resources.get(fetchKey);
83-
if(map===undefined){
84-
map=newMap();
85-
resources.set(fetchKey,map);
86-
}
87-
returnmap;
81+
functioncreateResultMap(): Map<string,Result<FetchResponse>>{
82+
returnnewMap();
8883
}
8984

9085
functionreadResult<T>(result: Result<T>): T {
@@ -166,7 +161,7 @@ Response.prototype = {
166161
};
167162

168163
functionpreloadResult(url: string,options: mixed): Result<FetchResponse>{
169-
constmap=readResultMap();
164+
constmap=getResultMap();
170165
letentry=map.get(url);
171166
if(!entry){
172167
if(options){

‎packages/react-fetch/src/__tests__/ReactFetchNode-test.js‎

Lines changed: 73 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,86 +10,112 @@
1010
'use strict';
1111

1212
describe('ReactFetchNode',()=>{
13-
letReactCache;
14-
letReactFetchNode;
1513
lethttp;
1614
letfetch;
15+
letwaitForSuspense;
1716
letserver;
1817
letserverEndpoint;
1918
letserverImpl;
2019

2120
beforeEach(done=>{
2221
jest.resetModules();
23-
if(__EXPERIMENTAL__){
24-
ReactCache=require('react/unstable-cache');
25-
// TODO: A way to pass load context.
26-
ReactCache.CacheProvider._context._currentValue=ReactCache.createCache();
27-
ReactFetchNode=require('react-fetch');
28-
fetch=ReactFetchNode.fetch;
29-
}
22+
23+
fetch=require('react-fetch').fetch;
3024
http=require('http');
25+
waitForSuspense=require('react-suspense-test-utils').waitForSuspense;
3126

3227
server=http.createServer((req,res)=>{
3328
serverImpl(req,res);
3429
});
35-
server.listen(done);
36-
serverEndpoint=`http://localhost:${server.address().port}/`;
30+
serverEndpoint=null;
31+
server.listen(()=>{
32+
serverEndpoint=`http://localhost:${server.address().port}/`;
33+
done();
34+
});
3735
});
3836

3937
afterEach(done=>{
4038
server.close(done);
4139
server=null;
4240
});
4341

44-
asyncfunctionwaitForSuspense(fn){
45-
while(true){
46-
try{
47-
returnfn();
48-
}catch(promise){
49-
if(typeofpromise.then==='function'){
50-
awaitpromise;
51-
}else{
52-
throwpromise;
53-
}
54-
}
55-
}
56-
}
42+
// @gate experimental
43+
it('can fetch text from a server component',async()=>{
44+
serverImpl=(req,res)=>{
45+
res.write('mango');
46+
res.end();
47+
};
48+
consttext=awaitwaitForSuspense(()=>{
49+
returnfetch(serverEndpoint).text();
50+
});
51+
expect(text).toEqual('mango');
52+
});
5753

5854
// @gate experimental
59-
it('can read text',async()=>{
55+
it('can fetch json from a server component',async()=>{
6056
serverImpl=(req,res)=>{
61-
res.write('ok');
57+
res.write(JSON.stringify({name: 'Sema'}));
6258
res.end();
6359
};
64-
awaitwaitForSuspense(()=>{
65-
constresponse=fetch(serverEndpoint);
66-
expect(response.status).toBe(200);
67-
expect(response.statusText).toBe('OK');
68-
expect(response.ok).toBe(true);
69-
expect(response.text()).toEqual('ok');
70-
// Can read again:
71-
expect(response.text()).toEqual('ok');
60+
constjson=awaitwaitForSuspense(()=>{
61+
returnfetch(serverEndpoint).json();
7262
});
63+
expect(json).toEqual({name: 'Sema'});
7364
});
7465

7566
// @gate experimental
76-
it('can read json',async()=>{
67+
it('provides response status',async()=>{
7768
serverImpl=(req,res)=>{
7869
res.write(JSON.stringify({name: 'Sema'}));
7970
res.end();
8071
};
81-
awaitwaitForSuspense(()=>{
82-
constresponse=fetch(serverEndpoint);
83-
expect(response.status).toBe(200);
84-
expect(response.statusText).toBe('OK');
85-
expect(response.ok).toBe(true);
86-
expect(response.json()).toEqual({
87-
name: 'Sema',
88-
});
89-
// Can read again:
90-
expect(response.json()).toEqual({
91-
name: 'Sema',
92-
});
72+
constresponse=awaitwaitForSuspense(()=>{
73+
returnfetch(serverEndpoint);
74+
});
75+
expect(response).toMatchObject({
76+
status: 200,
77+
statusText: 'OK',
78+
ok: true,
9379
});
9480
});
81+
82+
// @gate experimental
83+
it('handles different paths',async()=>{
84+
serverImpl=(req,res)=>{
85+
switch(req.url){
86+
case'/banana':
87+
res.write('banana');
88+
break;
89+
case'/mango':
90+
res.write('mango');
91+
break;
92+
case'/orange':
93+
res.write('orange');
94+
break;
95+
}
96+
res.end();
97+
};
98+
constoutputs=awaitwaitForSuspense(()=>{
99+
return[
100+
fetch(serverEndpoint+'banana').text(),
101+
fetch(serverEndpoint+'mango').text(),
102+
fetch(serverEndpoint+'orange').text(),
103+
];
104+
});
105+
expect(outputs).toMatchObject(['banana','mango','orange']);
106+
});
107+
108+
// @gate experimental
109+
it('can produce an error',async()=>{
110+
serverImpl=(req,res)=>{};
111+
112+
expect.assertions(1);
113+
try{
114+
awaitwaitForSuspense(()=>{
115+
returnfetch('BOOM');
116+
});
117+
}catch(err){
118+
expect(err.message).toEqual('Invalid URL: BOOM');
119+
}
120+
});
95121
});

0 commit comments

Comments
 (0)