From 515326753b15eb247493b1b5c657eee1bc515337 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 29 Apr 2020 19:14:15 +0100 Subject: [PATCH] [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 --- fixtures/blocks/src/App.js | 3 +- fixtures/blocks/src/Comments.js | 6 +- fixtures/blocks/src/Post.js | 4 +- fixtures/blocks/src/lib/cache.js | 30 ------ fixtures/blocks/src/lib/data.js | 59 ------------ packages/react-cache/index.js | 2 +- .../src/{ReactCache.js => ReactCacheOld.js} | 0 ...rnal.js => ReactCacheOld-test.internal.js} | 0 packages/react-data/README.md | 12 +++ packages/react-data/fetch.js | 12 +++ packages/react-data/index.js | 12 +++ packages/react-data/npm/fetch.js | 7 ++ packages/react-data/npm/index.js | 7 ++ packages/react-data/package.json | 22 +++++ packages/react-data/src/ReactData.js | 12 +++ .../src/__tests__/ReactData-test.js | 23 +++++ .../src/__tests__/ReactDataFetch-test.js | 23 +++++ .../react-data/src/fetch/ReactDataFetch.js | 93 +++++++++++++++++++ packages/react/npm/unstable-cache.js | 7 ++ packages/react/package.json | 3 +- .../react/src/__tests__/ReactCache-test.js | 23 +++++ packages/react/src/cache/ReactCache.js | 43 +++++++++ packages/react/unstable-cache.js | 9 ++ scripts/error-codes/codes.json | 3 +- scripts/rollup/bundles.js | 50 +++++++++- 25 files changed, 367 insertions(+), 98 deletions(-) delete mode 100644 fixtures/blocks/src/lib/cache.js delete mode 100644 fixtures/blocks/src/lib/data.js rename packages/react-cache/src/{ReactCache.js => ReactCacheOld.js} (100%) rename packages/react-cache/src/__tests__/{ReactCache-test.internal.js => ReactCacheOld-test.internal.js} (100%) create mode 100644 packages/react-data/README.md create mode 100644 packages/react-data/fetch.js create mode 100644 packages/react-data/index.js create mode 100644 packages/react-data/npm/fetch.js create mode 100644 packages/react-data/npm/index.js create mode 100644 packages/react-data/package.json create mode 100644 packages/react-data/src/ReactData.js create mode 100644 packages/react-data/src/__tests__/ReactData-test.js create mode 100644 packages/react-data/src/__tests__/ReactDataFetch-test.js create mode 100644 packages/react-data/src/fetch/ReactDataFetch.js create mode 100644 packages/react/npm/unstable-cache.js create mode 100644 packages/react/src/__tests__/ReactCache-test.js create mode 100644 packages/react/src/cache/ReactCache.js create mode 100644 packages/react/unstable-cache.js diff --git a/fixtures/blocks/src/App.js b/fixtures/blocks/src/App.js index 39c27e0fc035..ae57697e0b2d 100644 --- a/fixtures/blocks/src/App.js +++ b/fixtures/blocks/src/App.js @@ -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}), diff --git a/fixtures/blocks/src/Comments.js b/fixtures/blocks/src/Comments.js index 25c455071c9f..c91b795fd45f 100644 --- a/fixtures/blocks/src/Comments.js +++ b/fixtures/blocks/src/Comments.js @@ -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) + ), }; } diff --git a/fixtures/blocks/src/Post.js b/fixtures/blocks/src/Post.js index 5d300a63ff3c..3ca854f22b03 100644 --- a/fixtures/blocks/src/Post.js +++ b/fixtures/blocks/src/Post.js @@ -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), }; } diff --git a/fixtures/blocks/src/lib/cache.js b/fixtures/blocks/src/lib/cache.js deleted file mode 100644 index 876b0cbd3617..000000000000 --- a/fixtures/blocks/src/lib/cache.js +++ /dev/null @@ -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; -} diff --git a/fixtures/blocks/src/lib/data.js b/fixtures/blocks/src/lib/data.js deleted file mode 100644 index e9cdbe5621a7..000000000000 --- a/fixtures/blocks/src/lib/data.js +++ /dev/null @@ -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(); - } -} diff --git a/packages/react-cache/index.js b/packages/react-cache/index.js index 1480da2e0a71..7b32ad81dc27 100644 --- a/packages/react-cache/index.js +++ b/packages/react-cache/index.js @@ -9,4 +9,4 @@ 'use strict'; -export * from './src/ReactCache'; +export * from './src/ReactCacheOld'; diff --git a/packages/react-cache/src/ReactCache.js b/packages/react-cache/src/ReactCacheOld.js similarity index 100% rename from packages/react-cache/src/ReactCache.js rename to packages/react-cache/src/ReactCacheOld.js diff --git a/packages/react-cache/src/__tests__/ReactCache-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js similarity index 100% rename from packages/react-cache/src/__tests__/ReactCache-test.internal.js rename to packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js diff --git a/packages/react-data/README.md b/packages/react-data/README.md new file mode 100644 index 000000000000..44295ba6735c --- /dev/null +++ b/packages/react-data/README.md @@ -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. diff --git a/packages/react-data/fetch.js b/packages/react-data/fetch.js new file mode 100644 index 000000000000..9b33b056846b --- /dev/null +++ b/packages/react-data/fetch.js @@ -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'; diff --git a/packages/react-data/index.js b/packages/react-data/index.js new file mode 100644 index 000000000000..4cc66061049d --- /dev/null +++ b/packages/react-data/index.js @@ -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'; diff --git a/packages/react-data/npm/fetch.js b/packages/react-data/npm/fetch.js new file mode 100644 index 000000000000..1b9f07068ab0 --- /dev/null +++ b/packages/react-data/npm/fetch.js @@ -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'); +} diff --git a/packages/react-data/npm/index.js b/packages/react-data/npm/index.js new file mode 100644 index 000000000000..2fd2e2df8857 --- /dev/null +++ b/packages/react-data/npm/index.js @@ -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'); +} diff --git a/packages/react-data/package.json b/packages/react-data/package.json new file mode 100644 index 000000000000..e27abf837217 --- /dev/null +++ b/packages/react-data/package.json @@ -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" + } +} diff --git a/packages/react-data/src/ReactData.js b/packages/react-data/src/ReactData.js new file mode 100644 index 000000000000..9b5230b768ab --- /dev/null +++ b/packages/react-data/src/ReactData.js @@ -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 +} diff --git a/packages/react-data/src/__tests__/ReactData-test.js b/packages/react-data/src/__tests__/ReactData-test.js new file mode 100644 index 000000000000..9826ba55cf6d --- /dev/null +++ b/packages/react-data/src/__tests__/ReactData-test.js @@ -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); + }); +}); diff --git a/packages/react-data/src/__tests__/ReactDataFetch-test.js b/packages/react-data/src/__tests__/ReactDataFetch-test.js new file mode 100644 index 000000000000..1c6785913c32 --- /dev/null +++ b/packages/react-data/src/__tests__/ReactDataFetch-test.js @@ -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); + }); +}); diff --git a/packages/react-data/src/fetch/ReactDataFetch.js b/packages/react-data/src/fetch/ReactDataFetch.js new file mode 100644 index 000000000000..ade15cdfa0fc --- /dev/null +++ b/packages/react-data/src/fetch/ReactDataFetch.js @@ -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 { + 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; + } +} diff --git a/packages/react/npm/unstable-cache.js b/packages/react/npm/unstable-cache.js new file mode 100644 index 000000000000..ca819bd09295 --- /dev/null +++ b/packages/react/npm/unstable-cache.js @@ -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'); +} diff --git a/packages/react/package.json b/packages/react/package.json index a778afc30055..29d293bf231e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -16,7 +16,8 @@ "cjs/", "umd/", "jsx-runtime.js", - "jsx-dev-runtime.js" + "jsx-dev-runtime.js", + "unstable-cache.js" ], "main": "index.js", "repository": { diff --git a/packages/react/src/__tests__/ReactCache-test.js b/packages/react/src/__tests__/ReactCache-test.js new file mode 100644 index 000000000000..543fd7055ae6 --- /dev/null +++ b/packages/react/src/__tests__/ReactCache-test.js @@ -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); + }); +}); diff --git a/packages/react/src/cache/ReactCache.js b/packages/react/src/cache/ReactCache.js new file mode 100644 index 000000000000..d4240e1dca8a --- /dev/null +++ b/packages/react/src/cache/ReactCache.js @@ -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, +|}; + +// TODO: should there be a default cache? +const CacheContext: ReactContext = 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}; diff --git a/packages/react/unstable-cache.js b/packages/react/unstable-cache.js new file mode 100644 index 000000000000..a6b90aa6728a --- /dev/null +++ b/packages/react/unstable-cache.js @@ -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'; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 2faadd2e5460..641c43e2cad8 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -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." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 8320b9bdc6bd..f2d94cc8121a 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -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,