mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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:
1 parent
53d68b33ff
commit
515326753b
25 files changed
+367
-98
No files matched your search
@@ -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}),
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
export * from './src/ReactCache';
|
||||
export * from './src/ReactCacheOld';
|
||||
File renamed without changes.
File renamed without changes.
@@ -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.
|
||||
Vendored
+12
@@ -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';
|
||||
Vendored
+12
@@ -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';
|
||||
Vendored
+7
@@ -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');
|
||||
}
|
||||
Vendored
+7
@@ -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');
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+12
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
@@ -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};
|
||||
@@ -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';
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user