Skip to content

Commit 4bb19da

Browse files
committed
Detect refreshes by comparing to previous parent
Removes the fresh/stale distinction from the context stack and instead detects refreshes by comparing the previous and next parent cache. This is closer to one of the earlier implementation drafts, and it's essentially how you'd implement this in userspace using context. I had moved away from this when I got off on a tangent thinking about how the cache pool should work; once that fell into place, it became more clear what the relationship is between the context stack, which you use for updates ("Here"), and the cache pool, which you use for newly mounted content ("There"). The only thing we're doing internally that can't really be achieved in userspace is transfering the cache across Suspense retries. Kinda neat.
1 parent 3cfe44e commit 4bb19da

17 files changed

+507
-982
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 125 additions & 215 deletions
Large diffs are not rendered by default.

packages/react-reconciler/src/ReactFiberBeginWork.old.js

Lines changed: 125 additions & 215 deletions
Large diffs are not rendered by default.

packages/react-reconciler/src/ReactFiberCacheComponent.new.js

Lines changed: 100 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,27 @@
1010
import type {ReactContext} from 'shared/ReactTypes';
1111
import type {FiberRoot} from './ReactInternalTypes';
1212
import type {Lanes} from './ReactFiberLane.new';
13+
import type {StackCursor} from './ReactFiberStack.new';
1314

1415
import {enableCache} from 'shared/ReactFeatureFlags';
1516
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
16-
import {HostRoot} from './ReactWorkTags';
1717

18+
import {isPrimaryRenderer} from './ReactFiberHostConfig';
19+
import {createCursor, push, pop} from './ReactFiberStack.new';
1820
import {pushProvider, popProvider} from './ReactFiberNewContext.new';
1921

2022
export type Cache = Map<() => mixed, mixed>;
2123

22-
export type SuspendedCacheFresh = {|
23-
tag: 0,
24-
cache: Cache,
24+
export type CacheComponentState = {|
25+
+parent: Cache,
26+
+cache: Cache,
2527
|};
2628

27-
export type SuspendedCachePool = {|
28-
tag: 1,
29-
cache: Cache,
29+
export type SpawnedCachePool = {|
30+
+parent: Cache,
31+
+pool: Cache,
3032
|};
3133

32-
export type SuspendedCache = SuspendedCacheFresh | SuspendedCachePool;
33-
34-
export const SuspendedCacheFreshTag = 0;
35-
export const SuspendedCachePoolTag = 1;
36-
3734
export const CacheContext: ReactContext<Cache> = enableCache
3835
? {
3936
$$typeof: REACT_CONTEXT_TYPE,
@@ -53,77 +50,28 @@ if (__DEV__ && enableCache) {
5350
CacheContext._currentRenderer2 = null;
5451
}
5552

56-
// A parent cache refresh always overrides any nested cache. So there will only
57-
// ever be a single fresh cache on the context stack.
58-
let freshCache: Cache | null = null;
59-
60-
// The cache that we retrived from the pool during this render, if any
53+
// The cache that newly mounted Cache boundaries should use. It's either
54+
// retrieved from the cache pool, or the result of a refresh.
6155
let pooledCache: Cache | null = null;
6256

63-
export function pushStaleCacheProvider(workInProgress: Fiber, cache: Cache) {
64-
if (!enableCache) {
65-
return;
66-
}
67-
if (__DEV__) {
68-
if (freshCache !== null) {
69-
console.error(
70-
'Already inside a fresh cache boundary. This is a bug in React.',
71-
);
72-
}
73-
}
74-
pushProvider(workInProgress, CacheContext, cache);
75-
}
57+
// When retrying a Suspense/Offscreen boundary, we override pooledCache with the
58+
// cache from the render that suspended.
59+
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
7660

77-
export function pushFreshCacheProvider(workInProgress: Fiber, cache: Cache) {
61+
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
7862
if (!enableCache) {
7963
return;
8064
}
81-
if (__DEV__) {
82-
if (
83-
freshCache !== null &&
84-
// TODO: Remove this exception for roots. There are a few tests that throw
85-
// in pushHostContainer, before the cache context is pushed. Not a huge
86-
// issue, but should still fix.
87-
workInProgress.tag !== HostRoot
88-
) {
89-
console.error(
90-
'Already inside a fresh cache boundary. This is a bug in React.',
91-
);
92-
}
93-
}
94-
freshCache = cache;
9565
pushProvider(workInProgress, CacheContext, cache);
9666
}
9767

9868
export function popCacheProvider(workInProgress: Fiber, cache: Cache) {
9969
if (!enableCache) {
10070
return;
10171
}
102-
if (__DEV__) {
103-
if (freshCache !== null && freshCache !== cache) {
104-
console.error(
105-
'Unexpected cache instance on context. This is a bug in React.',
106-
);
107-
}
108-
}
109-
freshCache = null;
11072
popProvider(CacheContext, workInProgress);
11173
}
11274

113-
export function hasFreshCacheProvider() {
114-
if (!enableCache) {
115-
return false;
116-
}
117-
return freshCache !== null;
118-
}
119-
120-
export function getFreshCacheProviderIfExists(): Cache | null {
121-
if (!enableCache) {
122-
return null;
123-
}
124-
return freshCache;
125-
}
126-
12775
export function requestCacheFromPool(renderLanes: Lanes): Cache {
12876
if (!enableCache) {
12977
return (null: any);
@@ -136,10 +84,6 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
13684
return pooledCache;
13785
}
13886

139-
export function getPooledCacheIfExists(): Cache | null {
140-
return pooledCache;
141-
}
142-
14387
export function pushRootCachePool(root: FiberRoot) {
14488
if (!enableCache) {
14589
return;
@@ -161,37 +105,100 @@ export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
161105
// once all the transitions that depend on it (which we track with
162106
// `pooledCacheLanes`) have committed.
163107
root.pooledCache = pooledCache;
164-
root.pooledCacheLanes |= renderLanes;
108+
if (pooledCache !== null) {
109+
root.pooledCacheLanes |= renderLanes;
110+
}
165111
}
166112

167-
export function pushCachePool(suspendedCache: SuspendedCachePool) {
113+
export function restoreSpawnedCachePool(
114+
offscreenWorkInProgress: Fiber,
115+
prevCachePool: SpawnedCachePool,
116+
): SpawnedCachePool | null {
168117
if (!enableCache) {
169-
return;
118+
return (null: any);
119+
}
120+
const nextParentCache = isPrimaryRenderer
121+
? CacheContext._currentValue
122+
: CacheContext._currentValue2;
123+
if (nextParentCache !== prevCachePool.parent) {
124+
// There was a refresh. Don't bother restoring anything since the refresh
125+
// will override it.
126+
return null;
127+
} else {
128+
// No refresh. Resume with the previous cache. This will override the cache
129+
// pool so that any new Cache boundaries in the subtree use this one instead
130+
// of requesting a fresh one.
131+
push(prevFreshCacheOnStack, pooledCache, offscreenWorkInProgress);
132+
pooledCache = prevCachePool.pool;
133+
134+
// Return the cache pool to signal that we did in fact push it. We will
135+
// assign this to the field on the fiber so we know to pop the context.
136+
return prevCachePool;
170137
}
171-
// This will temporarily override the pooled cache for this render, so that
172-
// any new Cache boundaries in the subtree use this one. The previous value on
173-
// the "stack" is stored on the cache instance. We will restore it during the
174-
// complete phase.
175-
//
176-
// The more straightforward way to do this would be to use the array-based
177-
// stack (push/pop). Maybe this is too clever.
178-
const prevPooledCacheOnStack = pooledCache;
179-
pooledCache = suspendedCache.cache;
180-
// This is never supposed to be null. I'm cheating. Sorry. It will be reset to
181-
// the correct type when we pop.
182-
suspendedCache.cache = ((prevPooledCacheOnStack: any): Cache);
183138
}
184139

185-
export function popCachePool(suspendedCache: SuspendedCachePool) {
140+
// Note: Ideally, `popCachePool` would return this value, and then we would pass
141+
// it to `getSuspendedCachePool`. But factoring reasons, those two functions are
142+
// in different phases/files. They are always called in sequence, though, so we
143+
// can stash the value here temporarily.
144+
let _suspendedPooledCache: Cache | null = null;
145+
146+
export function popCachePool(workInProgress: Fiber) {
186147
if (!enableCache) {
187148
return;
188149
}
189-
const retryCache: Cache = (pooledCache: any);
190-
if (__DEV__) {
191-
if (retryCache === null) {
192-
console.error('Expected to have a pooled cache. This is a bug in React.');
150+
_suspendedPooledCache = pooledCache;
151+
pooledCache = prevFreshCacheOnStack.current;
152+
pop(prevFreshCacheOnStack, workInProgress);
153+
}
154+
155+
export function getSuspendedCachePool(): SpawnedCachePool | null {
156+
if (!enableCache) {
157+
return null;
158+
}
159+
160+
// We check the cache on the stack first, since that's the one any new Caches
161+
// would have accessed.
162+
let pool = pooledCache;
163+
if (pool === null) {
164+
// There's no pooled cache above us in the stack. However, a child in the
165+
// suspended tree may have requested a fresh cache pool. If so, we would
166+
// have unwound it with `popCachePool`.
167+
if (_suspendedPooledCache !== null) {
168+
pool = _suspendedPooledCache;
169+
_suspendedPooledCache = null;
170+
} else {
171+
// There's no suspended cache pool.
172+
return null;
193173
}
194174
}
195-
pooledCache = suspendedCache.cache;
196-
suspendedCache.cache = retryCache;
175+
176+
return {
177+
// We must also save the parent, so that when we resume we can detect
178+
// a refresh.
179+
parent: isPrimaryRenderer
180+
? CacheContext._currentValue
181+
: CacheContext._currentValue2,
182+
pool,
183+
};
184+
}
185+
186+
export function getOffscreenDeferredCachePool(): SpawnedCachePool | null {
187+
if (!enableCache) {
188+
return null;
189+
}
190+
191+
if (pooledCache === null) {
192+
// There's no deferred cache pool.
193+
return null;
194+
}
195+
196+
return {
197+
// We must also store the parent, so that when we resume we can detect
198+
// a refresh.
199+
parent: isPrimaryRenderer
200+
? CacheContext._currentValue
201+
: CacheContext._currentValue2,
202+
pool: pooledCache,
203+
};
197204
}

0 commit comments

Comments
 (0)