Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache() API #25506

Merged
merged 2 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 223 additions & 60 deletions packages/react-reconciler/src/__tests__/ReactCache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ let React;
let ReactNoop;
let Cache;
let getCacheSignal;
let getCacheForType;
let Scheduler;
let act;
let Suspense;
let Offscreen;
let useCacheRefresh;
let startTransition;
let useState;
let cache;

let caches;
let getTextCache;
let textCaches;
let seededCache;

describe('ReactCache', () => {
Expand All @@ -24,66 +25,68 @@ describe('ReactCache', () => {
Scheduler = require('scheduler');
act = require('jest-react').act;
Suspense = React.Suspense;
cache = React.experimental_cache;
Offscreen = React.unstable_Offscreen;
getCacheSignal = React.unstable_getCacheSignal;
getCacheForType = React.unstable_getCacheForType;
useCacheRefresh = React.unstable_useCacheRefresh;
startTransition = React.startTransition;
useState = React.useState;

caches = [];
textCaches = [];
seededCache = null;
});

function createTextCache() {
if (seededCache !== null) {
// Trick to seed a cache before it exists.
// TODO: Need a built-in API to seed data before the initial render (i.e.
// not a refresh because nothing has mounted yet).
const cache = seededCache;
seededCache = null;
return cache;
}

const data = new Map();
const version = caches.length + 1;
const cache = {
version,
data,
resolve(text) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.resolve();
if (gate(flags => flags.enableCache)) {
getTextCache = cache(() => {
if (seededCache !== null) {
// Trick to seed a cache before it exists.
// TODO: Need a built-in API to seed data before the initial render (i.e.
// not a refresh because nothing has mounted yet).
const textCache = seededCache;
seededCache = null;
return textCache;
}
},
reject(text, error) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'rejected',
value: error,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.reject();
}
},
};
caches.push(cache);
return cache;
}

const data = new Map();
const version = textCaches.length + 1;
const textCache = {
version,
data,
resolve(text) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.resolve();
}
},
reject(text, error) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'rejected',
value: error,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.reject();
}
},
};
textCaches.push(textCache);
return textCache;
});
}
});

function readText(text) {
const signal = getCacheSignal();
const textCache = getCacheForType(createTextCache);
const textCache = getTextCache();
const record = textCache.data.get(text);
if (record !== undefined) {
if (!record.cleanupScheduled) {
Expand Down Expand Up @@ -160,18 +163,18 @@ describe('ReactCache', () => {

function seedNextTextCache(text) {
if (seededCache === null) {
seededCache = createTextCache();
seededCache = getTextCache();
}
seededCache.resolve(text);
}

function resolveMostRecentTextCache(text) {
if (caches.length === 0) {
if (textCaches.length === 0) {
throw Error('Cache does not exist.');
} else {
// Resolve the most recently created cache. An older cache can by
// resolved with `caches[index].resolve(text)`.
caches[caches.length - 1].resolve(text);
// resolved with `textCaches[index].resolve(text)`.
textCaches[textCaches.length - 1].resolve(text);
}
}

Expand Down Expand Up @@ -815,9 +818,18 @@ describe('ReactCache', () => {

// @gate experimental || www
test('refresh a cache with seed data', async () => {
let refresh;
let refreshWithSeed;
function App() {
refresh = useCacheRefresh();
const refresh = useCacheRefresh();
const [seed, setSeed] = useState({fn: null});
if (seed.fn) {
seed.fn();
seed.fn = null;
}
refreshWithSeed = fn => {
setSeed({fn});
refresh();
};
return <AsyncText showVersion={true} text="A" />;
}

Expand Down Expand Up @@ -845,11 +857,14 @@ describe('ReactCache', () => {
await act(async () => {
// Refresh the cache with seeded data, like you would receive from a
// server mutation.
// TODO: Seeding multiple typed caches. Should work by calling `refresh`
// TODO: Seeding multiple typed textCaches. Should work by calling `refresh`
// multiple times with different key/value pairs
const cache = createTextCache();
cache.resolve('A');
startTransition(() => refresh(createTextCache, cache));
startTransition(() =>
refreshWithSeed(() => {
const textCache = getTextCache();
textCache.resolve('A');
}),
);
});
// The root should re-render without a cache miss.
// The cache is not cleared up yet, since it's still reference by the root
Expand Down Expand Up @@ -1624,4 +1639,152 @@ describe('ReactCache', () => {
expect(Scheduler).toHaveYielded(['More']);
expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
});

// @gate enableCache
it('cache objects and primitive arguments and a mix of them', async () => {
const root = ReactNoop.createRoot();
const types = cache((a, b) => ({a: typeof a, b: typeof b}));
function Print({a, b}) {
return types(a, b).a + ' ' + types(a, b).b + ' ';
}
function Same({a, b}) {
const x = types(a, b);
const y = types(a, b);
return (x === y).toString() + ' ';
}
function FlippedOrder({a, b}) {
return (types(a, b) === types(b, a)).toString() + ' ';
}
function FewerArgs({a, b}) {
return (types(a, b) === types(a)).toString() + ' ';
}
function MoreArgs({a, b}) {
return (types(a) === types(a, b)).toString() + ' ';
}
await act(async () => {
root.render(
<>
<Print a="e" b="f" />
<Same a="a" b="b" />
<FlippedOrder a="c" b="d" />
<FewerArgs a="e" b="f" />
<MoreArgs a="g" b="h" />
</>,
);
});
expect(root).toMatchRenderedOutput('string string true false false false ');
await act(async () => {
root.render(
<>
<Print a="e" b={null} />
<Same a="a" b={null} />
<FlippedOrder a="c" b={null} />
<FewerArgs a="e" b={null} />
<MoreArgs a="g" b={null} />
</>,
);
});
expect(root).toMatchRenderedOutput('string object true false false false ');
const obj = {};
await act(async () => {
root.render(
<>
<Print a="e" b={obj} />
<Same a="a" b={obj} />
<FlippedOrder a="c" b={obj} />
<FewerArgs a="e" b={obj} />
<MoreArgs a="g" b={obj} />
</>,
);
});
expect(root).toMatchRenderedOutput('string object true false false false ');
const sameObj = {};
await act(async () => {
root.render(
<>
<Print a={sameObj} b={sameObj} />
<Same a={sameObj} b={sameObj} />
<FlippedOrder a={sameObj} b={sameObj} />
<FewerArgs a={sameObj} b={sameObj} />
<MoreArgs a={sameObj} b={sameObj} />
</>,
);
});
expect(root).toMatchRenderedOutput('object object true true false false ');
const objA = {};
const objB = {};
await act(async () => {
root.render(
<>
<Print a={objA} b={objB} />
<Same a={objA} b={objB} />
<FlippedOrder a={objA} b={objB} />
<FewerArgs a={objA} b={objB} />
<MoreArgs a={objA} b={objB} />
</>,
);
});
expect(root).toMatchRenderedOutput('object object true false false false ');
const sameSymbol = Symbol();
await act(async () => {
root.render(
<>
<Print a={sameSymbol} b={sameSymbol} />
<Same a={sameSymbol} b={sameSymbol} />
<FlippedOrder a={sameSymbol} b={sameSymbol} />
<FewerArgs a={sameSymbol} b={sameSymbol} />
<MoreArgs a={sameSymbol} b={sameSymbol} />
</>,
);
});
expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
const notANumber = +'nan';
await act(async () => {
root.render(
<>
<Print a={1} b={notANumber} />
<Same a={1} b={notANumber} />
<FlippedOrder a={1} b={notANumber} />
<FewerArgs a={1} b={notANumber} />
<MoreArgs a={1} b={notANumber} />
</>,
);
});
expect(root).toMatchRenderedOutput('number number true false false false ');
});

// @gate enableCache
it('cached functions that throw should cache the error', async () => {
const root = ReactNoop.createRoot();
const throws = cache(v => {
throw new Error(v);
});
let x;
let y;
let z;
function Test() {
try {
throws(1);
} catch (e) {
x = e;
}
try {
throws(1);
} catch (e) {
y = e;
}
try {
throws(2);
} catch (e) {
z = e;
}

return 'Blank';
}
await act(async () => {
root.render(<Test />);
});
expect(x).toBe(y);
expect(z).not.toBe(x);
});
});
1 change: 1 addition & 0 deletions packages/react/index.classic.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
isValidElement,
lazy,
memo,
experimental_cache,
startTransition,
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
unstable_Cache,
Expand Down
1 change: 1 addition & 0 deletions packages/react/index.experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
isValidElement,
lazy,
memo,
experimental_cache,
startTransition,
unstable_Cache,
unstable_DebugTracingMode,
Expand Down
1 change: 1 addition & 0 deletions packages/react/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export {
isValidElement,
lazy,
memo,
experimental_cache,
startTransition,
unstable_Cache,
unstable_DebugTracingMode,
Expand Down
1 change: 1 addition & 0 deletions packages/react/index.modern.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
isValidElement,
lazy,
memo,
experimental_cache,
startTransition,
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
unstable_Cache,
Expand Down
Loading