[Blocks] Initial implementation of cache and data/fetch (#18774)

* Rename ReactCache -> ReactCacheOld

We still use it in some tests so I'm going to leave it for now. I'll start making the new one in parallel in the react package.

* Add react/unstable-cache entry point

* Add react-data entry point

* Initial implementation of cache and data/fetch

* Address review
This commit is contained in:
Dan Abramov authored and GitHub committed 2020-04-29 19:14:15 +01:00
1 parent 53d68b33ff
commit 515326753b
25 files changed
+367 -98

No files matched your search

+2 -1
View File
@@ -7,9 +7,10 @@
import React, {useReducer, useTransition, Suspense} from 'react';
import loadPost from './Post';
import {createCache, CacheProvider} from './lib/cache';
import {createCache, CacheProvider} from 'react/unstable-cache';
const initialState = {
// TODO: use this for invalidation.
cache: createCache(),
params: {id: 1},
RootBlock: loadPost({id: 1}),
+4 -2
View File
@@ -6,11 +6,13 @@
*/
import * as React from 'react';
import {fetch} from './lib/data';
import {fetch} from 'react-data/fetch';
function load(postId) {
return {
comments: fetch('http://localhost:3001/comments?postId=' + postId),
comments: JSON.parse(
fetch('http://localhost:3001/comments?postId=' + postId)
),
};
}
+2 -2
View File
@@ -7,12 +7,12 @@
import * as React from 'react';
import {block, Suspense} from 'react';
import {fetch} from './lib/data';
import {fetch} from 'react-data/fetch';
import loadComments from './Comments';
function load(params) {
return {
post: fetch('http://localhost:3001/posts/' + params.id),
post: JSON.parse(fetch('http://localhost:3001/posts/' + params.id)),
Comments: loadComments(params.id),
};
}
-30
View File
@@ -1,30 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {createContext} from 'react';
// TODO: clean up and move to react/cache.
// TODO: cancellation token.
// TODO: does there need to be default context?
const CacheContext = createContext(null);
export const CacheProvider = CacheContext.Provider;
// TODO: use this for invalidation.
export function createCache() {
return new Map();
}
export function readCache() {
// TODO: this doesn't subscribe.
// But we really want load context anyway.
return CacheContext._currentValue;
}
-59
View File
@@ -1,59 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {readCache} from './cache';
// TODO: clean up and move to react-data/fetch.
// TODO: some other data provider besides fetch.
// TODO: base agnostic helper like createResource. Maybe separate.
let sigil = {};
function readFetchMap() {
const cache = readCache();
if (!cache.has(sigil)) {
cache.set(sigil, new Map());
}
return cache.get(sigil);
}
export function fetch(url) {
const map = readFetchMap();
let entry = map.get(url);
if (entry === undefined) {
entry = {
status: 'pending',
result: new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
entry.result = JSON.parse(xhr.response);
entry.status = 'resolved';
resolve();
};
xhr.onerror = function(err) {
entry.result = err;
entry.status = 'rejected';
resolve();
};
xhr.open('GET', url);
xhr.send();
}),
};
map.set(url, entry);
}
switch (entry.status) {
case 'resolved':
return entry.result;
case 'pending':
case 'rejected':
throw entry.result;
default:
throw new Error();
}
}
+1 -1
View File
@@ -9,4 +9,4 @@
'use strict';
export * from './src/ReactCache';
export * from './src/ReactCacheOld';
File renamed without changes.
+12
View File
@@ -0,0 +1,12 @@
# react-data
This package is meant to be used alongside yet-to-be-released, experimental React features. It's unlikely to be useful in any other context.
**Do not use in a real application.** We're publishing this early for
demonstration purposes.
**Use it at your own risk.**
# No, Really, It Is Unstable
The API ~~may~~ will change wildly between versions.
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
export * from './src/fetch/ReactDataFetch';
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
export * from './src/ReactData';
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-data-fetch.production.min.js');
} else {
module.exports = require('./cjs/react-data-fetch.development.js');
}
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-data.production.min.js');
} else {
module.exports = require('./cjs/react-data.development.js');
}
+22
View File
@@ -0,0 +1,22 @@
{
"private": true,
"name": "react-data",
"description": "Helpers for creating React data sources",
"version": "0.0.0",
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-data"
},
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"fetch.js",
"cjs/"
],
"peerDependencies": {
"react": "^16.13.1"
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export function createResource(): any {
// TODO
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('ReactData', () => {
let ReactData;
beforeEach(() => {
ReactData = require('react-data');
});
// TODO: test something useful.
it('exports something', () => {
expect(ReactData.createResource).not.toBe(undefined);
});
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('ReactDataFetch', () => {
let ReactDataFetch;
beforeEach(() => {
ReactDataFetch = require('react-data/fetch');
});
// TODO: test something useful.
it('exports something', () => {
expect(ReactDataFetch.fetch).not.toBe(undefined);
});
});
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import {readCache} from 'react/unstable-cache';
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingResult = {|
status: 0,
value: Wakeable,
|};
type ResolvedResult = {|
status: 1,
value: mixed,
|};
type RejectedResult = {|
status: 2,
value: mixed,
|};
type Result = PendingResult | ResolvedResult | RejectedResult;
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;
}
// TODO: options, auth, etc.
export function fetch(url: string): Object {
const map = readResultMap();
const entry = map.get(url);
if (entry === undefined) {
let resolve = () => {};
const wakeable: Wakeable = new Promise(r => {
// TODO: should this be a plain thenable instead?
resolve = r;
});
const result: Result = {
status: Pending,
value: wakeable,
};
map.set(url, result);
const xhr = new XMLHttpRequest();
xhr.onload = function() {
// TODO: should we handle status codes?
if (result.status !== Pending) {
return;
}
const resolvedResult = ((result: any): ResolvedResult);
resolvedResult.status = Resolved;
resolvedResult.value = xhr.response;
resolve();
};
xhr.onerror = function() {
if (result.status !== Pending) {
return;
}
const rejectedResult = ((result: any): RejectedResult);
rejectedResult.status = Rejected;
// TODO: use something else as the error value?
rejectedResult.value = xhr;
resolve();
};
xhr.open('GET', url);
xhr.send();
throw wakeable;
}
const result: Result = entry;
if (result.status === Resolved) {
return result.value;
} else {
throw result.value;
}
}
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-unstable-cache.production.min.js');
} else {
module.exports = require('./cjs/react-unstable-cache.development.js');
}
+2 -1
View File
@@ -16,7 +16,8 @@
"cjs/",
"umd/",
"jsx-runtime.js",
"jsx-dev-runtime.js"
"jsx-dev-runtime.js",
"unstable-cache.js"
],
"main": "index.js",
"repository": {
@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('ReactCache', () => {
let ReactCache;
beforeEach(() => {
ReactCache = require('react/unstable-cache');
});
// TODO: test something useful.
it('exports something', () => {
expect(ReactCache.readCache).not.toBe(undefined);
});
});
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @flow
*/
import type {ReactContext} from 'shared/ReactTypes';
import {createContext} from 'react';
import invariant from 'shared/invariant';
type Cache = {|
resources: Map<any, any>,
|};
// TODO: should there be a default cache?
const CacheContext: ReactContext<null | Cache> = createContext(null);
function CacheImpl() {
this.resources = new Map();
// TODO: cancellation token.
}
function createCache(): Cache {
// $FlowFixMe
return new CacheImpl();
}
function readCache(): Cache {
// TODO: this doesn't subscribe.
// But we really want load context anyway.
const value = CacheContext._currentValue;
if (value instanceof CacheImpl) {
return value;
}
invariant(false, 'Could not read the cache.');
}
const CacheProvider = CacheContext.Provider;
export {createCache, readCache, CacheProvider};
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export {createCache, readCache, CacheProvider} from './src/cache/ReactCache';
+2 -1
View File
@@ -352,5 +352,6 @@
"352": "React Blocks (and Lazy Components) are expected to be replaced by a compiler on the server. Try configuring your compiler set up and avoid using React.lazy inside of Blocks.",
"353": "A server block should never encode any other slots. This is a bug in React.",
"354": "getInspectorDataForViewAtPoint() is not available in production.",
"355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly."
"355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.",
"356": "Could not read the cache."
}
+49 -1
View File
@@ -108,6 +108,54 @@ const bundles = [
externals: ['react'],
},
/******* React Cache (experimental, new) *******/
{
bundleTypes: [
NODE_DEV,
NODE_PROD,
NODE_PROFILING,
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
],
moduleType: ISOMORPHIC,
entry: 'react/unstable-cache',
global: 'ReactCache',
externals: ['react'],
},
/******* React Data (experimental, new) *******/
{
bundleTypes: [
NODE_DEV,
NODE_PROD,
NODE_PROFILING,
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
],
moduleType: ISOMORPHIC,
entry: 'react-data',
global: 'ReactData',
externals: ['react'],
},
/******* React Data Fetch (experimental, new) *******/
{
bundleTypes: [
NODE_DEV,
NODE_PROD,
NODE_PROFILING,
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
],
moduleType: ISOMORPHIC,
entry: 'react-data/fetch',
global: 'ReactDataFetch',
externals: ['react', 'react-data'],
},
/******* React DOM *******/
{
bundleTypes: [
@@ -529,7 +577,7 @@ const bundles = [
externals: [],
},
/******* React Cache (experimental) *******/
/******* React Cache (experimental, old) *******/
{
bundleTypes: [
FB_WWW_DEV,