Skip to content

Commit 047d95e

Browse files
authored
[crud] Basic implementation (#31523)
This PR introduces a new experimental hook `useResourceEffect`, which is something that we're doing some very early initial tests on. This may likely not pan out and will be removed or modified if so. Please do not rely on it as it will break.
1 parent 92c0f5f commit 047d95e

9 files changed

+1309
-33
lines changed

packages/react-reconciler/src/ReactFiberCallUserSpace.js

+53-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import type {CapturedValue} from './ReactCapturedValue';
1414

1515
import {isRendering, setIsRendering} from './ReactCurrentFiber';
1616
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
17+
import {
18+
ResourceEffectIdentityKind,
19+
ResourceEffectUpdateKind,
20+
} from './ReactFiberHooks';
21+
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
1722

1823
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
1924
// TODO: Consider marking the whole bundle instead of these boundaries.
@@ -176,12 +181,54 @@ export const callComponentWillUnmountInDEV: (
176181
: (null: any);
177182

178183
const callCreate = {
179-
'react-stack-bottom-frame': function (effect: Effect): (() => void) | void {
180-
const create = effect.create;
181-
const inst = effect.inst;
182-
const destroy = create();
183-
inst.destroy = destroy;
184-
return destroy;
184+
'react-stack-bottom-frame': function (
185+
effect: Effect,
186+
): (() => void) | mixed | void {
187+
if (!enableUseResourceEffectHook) {
188+
if (effect.resourceKind != null) {
189+
if (__DEV__) {
190+
console.error(
191+
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
192+
'got %s',
193+
effect.resourceKind,
194+
);
195+
}
196+
}
197+
const create = effect.create;
198+
const inst = effect.inst;
199+
// $FlowFixMe[not-a-function] (@poteto)
200+
const destroy = create();
201+
// $FlowFixMe[incompatible-type] (@poteto)
202+
inst.destroy = destroy;
203+
return destroy;
204+
} else {
205+
if (effect.resourceKind == null) {
206+
const create = effect.create;
207+
const inst = effect.inst;
208+
const destroy = create();
209+
inst.destroy = destroy;
210+
return destroy;
211+
}
212+
switch (effect.resourceKind) {
213+
case ResourceEffectIdentityKind: {
214+
return effect.create();
215+
}
216+
case ResourceEffectUpdateKind: {
217+
if (typeof effect.update === 'function') {
218+
effect.update(effect.inst.resource);
219+
}
220+
break;
221+
}
222+
default: {
223+
if (__DEV__) {
224+
console.error(
225+
'Unhandled Effect kind %s. This is a bug in React.',
226+
effect.kind,
227+
);
228+
}
229+
}
230+
}
231+
}
185232
},
186233
};
187234

packages/react-reconciler/src/ReactFiberCommitEffects.js

+154-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
enableProfilerNestedUpdatePhase,
1919
enableSchedulingProfiler,
2020
enableScopeAPI,
21+
enableUseResourceEffectHook,
2122
} from 'shared/ReactFeatureFlags';
2223
import {
2324
ClassComponent,
@@ -49,6 +50,7 @@ import {
4950
Layout as HookLayout,
5051
Insertion as HookInsertion,
5152
Passive as HookPassive,
53+
HasEffect as HookHasEffect,
5254
} from './ReactHookEffectTags';
5355
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
5456
import {
@@ -70,6 +72,10 @@ import {
7072
} from './ReactFiberCallUserSpace';
7173

7274
import {runWithFiberInDEV} from './ReactCurrentFiber';
75+
import {
76+
ResourceEffectIdentityKind,
77+
ResourceEffectUpdateKind,
78+
} from './ReactFiberHooks';
7379

7480
function shouldProfile(current: Fiber): boolean {
7581
return (
@@ -146,19 +152,90 @@ export function commitHookEffectListMount(
146152

147153
// Mount
148154
let destroy;
155+
if (enableUseResourceEffectHook) {
156+
if (effect.resourceKind === ResourceEffectIdentityKind) {
157+
if (__DEV__) {
158+
effect.inst.resource = runWithFiberInDEV(
159+
finishedWork,
160+
callCreateInDEV,
161+
effect,
162+
);
163+
if (effect.inst.resource == null) {
164+
console.error(
165+
'useResourceEffect must provide a callback which returns a resource. ' +
166+
'If a managed resource is not needed here, use useEffect. Received %s',
167+
effect.inst.resource,
168+
);
169+
}
170+
} else {
171+
effect.inst.resource = effect.create();
172+
}
173+
destroy = effect.inst.destroy;
174+
}
175+
if (effect.resourceKind === ResourceEffectUpdateKind) {
176+
if (
177+
// We don't want to fire updates on remount during Activity
178+
(flags & HookHasEffect) > 0 &&
179+
typeof effect.update === 'function' &&
180+
effect.inst.resource != null
181+
) {
182+
// TODO(@poteto) what about multiple updates?
183+
if (__DEV__) {
184+
runWithFiberInDEV(finishedWork, callCreateInDEV, effect);
185+
} else {
186+
effect.update(effect.inst.resource);
187+
}
188+
}
189+
}
190+
}
149191
if (__DEV__) {
150192
if ((flags & HookInsertion) !== NoHookEffect) {
151193
setIsRunningInsertionEffect(true);
152194
}
153-
destroy = runWithFiberInDEV(finishedWork, callCreateInDEV, effect);
195+
if (enableUseResourceEffectHook) {
196+
if (effect.resourceKind == null) {
197+
destroy = runWithFiberInDEV(
198+
finishedWork,
199+
callCreateInDEV,
200+
effect,
201+
);
202+
}
203+
} else {
204+
destroy = runWithFiberInDEV(
205+
finishedWork,
206+
callCreateInDEV,
207+
effect,
208+
);
209+
}
154210
if ((flags & HookInsertion) !== NoHookEffect) {
155211
setIsRunningInsertionEffect(false);
156212
}
157213
} else {
158-
const create = effect.create;
159-
const inst = effect.inst;
160-
destroy = create();
161-
inst.destroy = destroy;
214+
if (enableUseResourceEffectHook) {
215+
if (effect.resourceKind == null) {
216+
const create = effect.create;
217+
const inst = effect.inst;
218+
destroy = create();
219+
inst.destroy = destroy;
220+
}
221+
} else {
222+
if (effect.resourceKind != null) {
223+
if (__DEV__) {
224+
console.error(
225+
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
226+
'got %s',
227+
effect.resourceKind,
228+
);
229+
}
230+
}
231+
const create = effect.create;
232+
const inst = effect.inst;
233+
// $FlowFixMe[incompatible-type] (@poteto)
234+
// $FlowFixMe[not-a-function] (@poteto)
235+
destroy = create();
236+
// $FlowFixMe[incompatible-type] (@poteto)
237+
inst.destroy = destroy;
238+
}
162239
}
163240

164241
if (enableSchedulingProfiler) {
@@ -176,6 +253,11 @@ export function commitHookEffectListMount(
176253
hookName = 'useLayoutEffect';
177254
} else if ((effect.tag & HookInsertion) !== NoFlags) {
178255
hookName = 'useInsertionEffect';
256+
} else if (
257+
enableUseResourceEffectHook &&
258+
effect.resourceKind != null
259+
) {
260+
hookName = 'useResourceEffect';
179261
} else {
180262
hookName = 'useEffect';
181263
}
@@ -202,6 +284,7 @@ export function commitHookEffectListMount(
202284
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
203285
'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching';
204286
} else {
287+
// $FlowFixMe[unsafe-addition] (@poteto)
205288
addendum = ' You returned: ' + destroy;
206289
}
207290
runWithFiberInDEV(
@@ -246,7 +329,13 @@ export function commitHookEffectListUnmount(
246329
const inst = effect.inst;
247330
const destroy = inst.destroy;
248331
if (destroy !== undefined) {
249-
inst.destroy = undefined;
332+
if (enableUseResourceEffectHook) {
333+
if (effect.resourceKind == null) {
334+
inst.destroy = undefined;
335+
}
336+
} else {
337+
inst.destroy = undefined;
338+
}
250339
if (enableSchedulingProfiler) {
251340
if ((flags & HookPassive) !== NoHookEffect) {
252341
markComponentPassiveEffectUnmountStarted(finishedWork);
@@ -260,7 +349,41 @@ export function commitHookEffectListUnmount(
260349
setIsRunningInsertionEffect(true);
261350
}
262351
}
263-
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
352+
if (enableUseResourceEffectHook) {
353+
if (
354+
effect.resourceKind === ResourceEffectIdentityKind &&
355+
effect.inst.resource != null
356+
) {
357+
safelyCallDestroyWithResource(
358+
finishedWork,
359+
nearestMountedAncestor,
360+
destroy,
361+
effect.inst.resource,
362+
);
363+
if (effect.next.resourceKind === ResourceEffectUpdateKind) {
364+
// $FlowFixMe[prop-missing] (@poteto)
365+
effect.next.update = undefined;
366+
} else {
367+
if (__DEV__) {
368+
console.error(
369+
'Expected a ResourceEffectUpdateKind to follow ResourceEffectIdentityKind, ' +
370+
'got %s. This is a bug in React.',
371+
effect.next.resourceKind,
372+
);
373+
}
374+
}
375+
effect.inst.resource = null;
376+
}
377+
if (effect.resourceKind == null) {
378+
safelyCallDestroy(
379+
finishedWork,
380+
nearestMountedAncestor,
381+
destroy,
382+
);
383+
}
384+
} else {
385+
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
386+
}
264387
if (__DEV__) {
265388
if ((flags & HookInsertion) !== NoHookEffect) {
266389
setIsRunningInsertionEffect(false);
@@ -895,6 +1018,30 @@ function safelyCallDestroy(
8951018
}
8961019
}
8971020

1021+
function safelyCallDestroyWithResource(
1022+
current: Fiber,
1023+
nearestMountedAncestor: Fiber | null,
1024+
destroy: mixed => void,
1025+
resource: mixed,
1026+
) {
1027+
const destroy_ = resource == null ? destroy : destroy.bind(null, resource);
1028+
if (__DEV__) {
1029+
runWithFiberInDEV(
1030+
current,
1031+
callDestroyInDEV,
1032+
current,
1033+
nearestMountedAncestor,
1034+
destroy_,
1035+
);
1036+
} else {
1037+
try {
1038+
destroy_();
1039+
} catch (error) {
1040+
captureCommitPhaseError(current, nearestMountedAncestor, error);
1041+
}
1042+
}
1043+
}
1044+
8981045
function commitProfiler(
8991046
finishedWork: Fiber,
9001047
current: Fiber | null,

0 commit comments

Comments
 (0)