Skip to content

Commit

Permalink
[Flight] Add getCacheForType() to the dispatcher (#20315)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gaearon committed Dec 3, 2020
1 parent 555eeae commit e23673b
Show file tree
Hide file tree
Showing 37 changed files with 363 additions and 156 deletions.
21 changes: 18 additions & 3 deletions fixtures/flight/server/cli.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@ const app = express();
// Application
app.get('/', function(req, res) {
if (process.env.NODE_ENV === 'development') {
for (var key in require.cache) {
delete require.cache[key];
}
// This doesn't work in ESM mode.
// for (var key in require.cache) {
// delete require.cache[key];
// }
}
require('./handler.server.js')(req, res);
});

app.get('/todos', function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.json([
{
id: 1,
text: 'Shave yaks',
},
{
id: 2,
text: 'Eat kale',
},
]);
});

app.listen(3001, () => {
console.log('Flight Server listening on port 3001...');
});
Expand Down
7 changes: 7 additions & 0 deletions fixtures/flight/src/App.server.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import {fetch} from 'react-fetch';

import Container from './Container.js';

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

export default function App() {
const todos = fetch('http://localhost:3001/todos').json();
return (
<Container>
<h1>Hello, world</h1>
<Counter />
<Counter2 />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<ShowMore>
<p>Lorem ipsum</p>
</ShowMore>
Expand Down
6 changes: 6 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';

import ErrorStackParser from 'error-stack-parser';
import invariant from 'shared/invariant';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
import {
Expand Down Expand Up @@ -100,6 +101,10 @@ function nextHook(): null | Hook {
return hook;
}

function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
}

function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
Expand Down Expand Up @@ -298,6 +303,7 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
}

const Dispatcher: DispatcherType = {
getCacheForType,
readContext,
useCallback,
useContext,
Expand Down
9 changes: 9 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type PartialRenderer from './ReactPartialRenderer';
import {validateContextBounds} from './ReactPartialRendererContext';

import invariant from 'shared/invariant';
import {enableCache} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';

type BasicStateAction<S> = (S => S) | S;
Expand Down Expand Up @@ -214,6 +215,10 @@ export function resetHooksState(): void {
workInProgressHook = null;
}

function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
}

function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
Expand Down Expand Up @@ -512,3 +517,7 @@ export const Dispatcher: DispatcherType = {
// Subscriptions are not setup in a server environment.
useMutableSource,
};

if (enableCache) {
Dispatcher.getCacheForType = getCacheForType;
}
21 changes: 9 additions & 12 deletions packages/react-fetch/src/ReactFetchBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import type {Wakeable} from 'shared/ReactTypes';

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

const Pending = 0;
const Resolved = 1;
Expand All @@ -34,16 +34,13 @@ type Result = PendingResult | ResolvedResult | RejectedResult;

// TODO: this is a browser-only version. Add a separate Node entry point.
const nativeFetch = window.fetch;
const fetchKey = {};

function readResultMap(): Map<string, Result> {
const resources = readCache().resources;
let map = resources.get(fetchKey);
if (map === undefined) {
map = new Map();
resources.set(fetchKey, map);
}
return map;

function getResultMap(): Map<string, Result> {
return unstable_getCacheForType(createResultMap);
}

function createResultMap(): Map<string, Result> {
return new Map();
}

function toResult(thenable): Result {
Expand Down Expand Up @@ -120,7 +117,7 @@ Response.prototype = {
};

function preloadResult(url: string, options: mixed): Result {
const map = readResultMap();
const map = getResultMap();
let entry = map.get(url);
if (!entry) {
if (options) {
Expand Down
19 changes: 7 additions & 12 deletions packages/react-fetch/src/ReactFetchNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes';

import * as http from 'http';
import * as https from 'https';

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

type FetchResponse = {|
// Properties
Expand Down Expand Up @@ -75,16 +74,12 @@ type RejectedResult = {|

type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;

const fetchKey = {};
function getResultMap(): Map<string, Result<FetchResponse>> {
return unstable_getCacheForType(createResultMap);
}

function readResultMap(): Map<string, Result<FetchResponse>> {
const resources = readCache().resources;
let map = resources.get(fetchKey);
if (map === undefined) {
map = new Map();
resources.set(fetchKey, map);
}
return map;
function createResultMap(): Map<string, Result<FetchResponse>> {
return new Map();
}

function readResult<T>(result: Result<T>): T {
Expand Down Expand Up @@ -166,7 +161,7 @@ Response.prototype = {
};

function preloadResult(url: string, options: mixed): Result<FetchResponse> {
const map = readResultMap();
const map = getResultMap();
let entry = map.get(url);
if (!entry) {
if (options) {
Expand Down
120 changes: 73 additions & 47 deletions packages/react-fetch/src/__tests__/ReactFetchNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,86 +10,112 @@
'use strict';

describe('ReactFetchNode', () => {
let ReactCache;
let ReactFetchNode;
let http;
let fetch;
let waitForSuspense;
let server;
let serverEndpoint;
let serverImpl;

beforeEach(done => {
jest.resetModules();
if (__EXPERIMENTAL__) {
ReactCache = require('react/unstable-cache');
// TODO: A way to pass load context.
ReactCache.CacheProvider._context._currentValue = ReactCache.createCache();
ReactFetchNode = require('react-fetch');
fetch = ReactFetchNode.fetch;
}

fetch = require('react-fetch').fetch;
http = require('http');
waitForSuspense = require('react-suspense-test-utils').waitForSuspense;

server = http.createServer((req, res) => {
serverImpl(req, res);
});
server.listen(done);
serverEndpoint = `http://localhost:${server.address().port}/`;
serverEndpoint = null;
server.listen(() => {
serverEndpoint = `http://localhost:${server.address().port}/`;
done();
});
});

afterEach(done => {
server.close(done);
server = null;
});

async function waitForSuspense(fn) {
while (true) {
try {
return fn();
} catch (promise) {
if (typeof promise.then === 'function') {
await promise;
} else {
throw promise;
}
}
}
}
// @gate experimental
it('can fetch text from a server component', async () => {
serverImpl = (req, res) => {
res.write('mango');
res.end();
};
const text = await waitForSuspense(() => {
return fetch(serverEndpoint).text();
});
expect(text).toEqual('mango');
});

// @gate experimental
it('can read text', async () => {
it('can fetch json from a server component', async () => {
serverImpl = (req, res) => {
res.write('ok');
res.write(JSON.stringify({name: 'Sema'}));
res.end();
};
await waitForSuspense(() => {
const response = fetch(serverEndpoint);
expect(response.status).toBe(200);
expect(response.statusText).toBe('OK');
expect(response.ok).toBe(true);
expect(response.text()).toEqual('ok');
// Can read again:
expect(response.text()).toEqual('ok');
const json = await waitForSuspense(() => {
return fetch(serverEndpoint).json();
});
expect(json).toEqual({name: 'Sema'});
});

// @gate experimental
it('can read json', async () => {
it('provides response status', async () => {
serverImpl = (req, res) => {
res.write(JSON.stringify({name: 'Sema'}));
res.end();
};
await waitForSuspense(() => {
const response = fetch(serverEndpoint);
expect(response.status).toBe(200);
expect(response.statusText).toBe('OK');
expect(response.ok).toBe(true);
expect(response.json()).toEqual({
name: 'Sema',
});
// Can read again:
expect(response.json()).toEqual({
name: 'Sema',
});
const response = await waitForSuspense(() => {
return fetch(serverEndpoint);
});
expect(response).toMatchObject({
status: 200,
statusText: 'OK',
ok: true,
});
});

// @gate experimental
it('handles different paths', async () => {
serverImpl = (req, res) => {
switch (req.url) {
case '/banana':
res.write('banana');
break;
case '/mango':
res.write('mango');
break;
case '/orange':
res.write('orange');
break;
}
res.end();
};
const outputs = await waitForSuspense(() => {
return [
fetch(serverEndpoint + 'banana').text(),
fetch(serverEndpoint + 'mango').text(),
fetch(serverEndpoint + 'orange').text(),
];
});
expect(outputs).toMatchObject(['banana', 'mango', 'orange']);
});

// @gate experimental
it('can produce an error', async () => {
serverImpl = (req, res) => {};

expect.assertions(1);
try {
await waitForSuspense(() => {
return fetch('BOOM');
});
} catch (err) {
expect(err.message).toEqual('Invalid URL: BOOM');
}
});
});
Loading

0 comments on commit e23673b

Please sign in to comment.