[crud] Merge useResourceEffect into useEffect (#32205)
Merges the useResourceEffect API into useEffect while keeping the underlying implementation the same. useResourceEffect will be removed in the next diff. To fork between behavior we rely on a `typeof` check for the updater or destroy function in addition to the CRUD feature flag. This does now have to be checked every time (instead of inlined statically like before due to them being different hooks) which will incur some non-zero amount (possibly negligble) of overhead for every effect. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32205). * #32206 * __->__ #32205
lauren committed
Feb 11, 2025 at 14:18 UTC
2c5fd26c07c0fb94ff21a6c10c5a757ef3c5d6a4
9 files changed
+296
-134
packages/react-debug-tools/src/ReactDebugHooks.js
+5
-2
@@ -373,8 +373,11 @@ function useInsertionEffect(
373
}
374
375
function useEffect(
376
- create: () => (() => void) | void,
377
- inputs: Array<mixed> | void | null,
376
+ create: (() => (() => void) | void) | (() => {...} | void | null),
377
+ createDeps: Array<mixed> | void | null,
378
+ update?: ((resource: {...} | void | null) => void) | void,
379
+ updateDeps?: Array<mixed> | void | null,
380
+ destroy?: ((resource: {...} | void | null) => void) | void,
381
): void {
382
nextHook();
383
hookLog.push({
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+2
-4
@@ -27,7 +27,6 @@ let useRef;
27
let useImperativeHandle;
28
let useInsertionEffect;
29
let useLayoutEffect;
30
-let useResourceEffect;
30
let useDebugValue;
31
let forwardRef;
32
let yieldedValues;
@@ -52,7 +51,6 @@ function initModules() {
51
useImperativeHandle = React.useImperativeHandle;
52
useInsertionEffect = React.useInsertionEffect;
53
useLayoutEffect = React.useLayoutEffect;
55
- useResourceEffect = React.experimental_useResourceEffect;
54
forwardRef = React.forwardRef;
55
56
yieldedValues = [];
@@ -655,7 +653,7 @@ describe('ReactDOMServerHooks', () => {
653
});
654
});
655
658
- describe('useResourceEffect', () => {
656
+ describe('useEffect with CRUD overload', () => {
657
gate(flags => {
658
if (flags.enableUseEffectCRUDOverload) {
659
const yields = [];
@@ -663,7 +661,7 @@ describe('ReactDOMServerHooks', () => {
661
'should ignore resource effects on the server',
662
async render => {
663
function Counter(props) {
666
- useResourceEffect(
664
+ useEffect(
665
() => {
666
yieldValue('created on client');
667
return {resource_counter: props.count};
packages/react-reconciler/src/ReactFiberCallUserSpace.js
+1
-1
@@ -254,7 +254,7 @@ const callDestroy = {
254
export const callDestroyInDEV: (
255
current: Fiber,
256
nearestMountedAncestor: Fiber | null,
257
- destroy: () => void,
257
+ destroy: (() => void) | (({...}) => void),
258
) => void = __DEV__
259
? // We use this technique to trick minifiers to preserve the function name.
260
(callDestroy['react-stack-bottom-frame'].bind(callDestroy): any)
packages/react-reconciler/src/ReactFiberCommitEffects.js
+8
-33
@@ -170,8 +170,9 @@ export function commitHookEffectListMount(
170
);
171
if (effect.inst.resource == null) {
172
console.error(
173
- 'useResourceEffect must provide a callback which returns a resource. ' +
174
- 'If a managed resource is not needed here, use useEffect. Received %s',
173
+ 'useEffect must provide a callback which returns a resource. ' +
174
+ 'If a managed resource is not needed here, do not provide an updater or ' +
175
+ 'destroy callback. Received %s',
176
effect.inst.resource,
177
);
178
}
@@ -261,11 +262,6 @@ export function commitHookEffectListMount(
262
hookName = 'useLayoutEffect';
263
} else if ((effect.tag & HookInsertion) !== NoFlags) {
264
hookName = 'useInsertionEffect';
264
- } else if (
265
- enableUseEffectCRUDOverload &&
266
- effect.resourceKind != null
267
- ) {
268
- hookName = 'useResourceEffect';
265
} else {
266
hookName = 'useEffect';
267
}
@@ -363,7 +359,7 @@ export function commitHookEffectListUnmount(
359
effect.resourceKind === ResourceEffectIdentityKind &&
360
effect.inst.resource != null
361
) {
366
- safelyCallDestroyWithResource(
362
+ safelyCallDestroy(
363
finishedWork,
364
nearestMountedAncestor,
365
destroy,
@@ -1015,32 +1011,11 @@ export function safelyDetachRef(
1011
function safelyCallDestroy(
1012
current: Fiber,
1013
nearestMountedAncestor: Fiber | null,
1018
- destroy: () => void,
1019
-) {
1020
- if (__DEV__) {
1021
- runWithFiberInDEV(
1022
- current,
1023
- callDestroyInDEV,
1024
- current,
1025
- nearestMountedAncestor,
1026
- destroy,
1027
- );
1028
- } else {
1029
- try {
1030
- destroy();
1031
- } catch (error) {
1032
- captureCommitPhaseError(current, nearestMountedAncestor, error);
1033
- }
1034
- }
1035
-}
1036
-
1037
-function safelyCallDestroyWithResource(
1038
- current: Fiber,
1039
- nearestMountedAncestor: Fiber | null,
1040
- destroy: ({...}) => void,
1041
- resource: {...},
1014
+ destroy: (() => void) | (({...}) => void),
1015
+ resource?: {...} | void | null,
1016
) {
1043
- const destroy_ = destroy.bind(null, resource);
1017
+ // $FlowFixMe[extra-arg] @poteto this is safe either way because the extra arg is ignored if it's not a CRUD effect
1018
+ const destroy_ = resource == null ? destroy : destroy.bind(null, resource);
1019
if (__DEV__) {
1020
runWithFiberInDEV(
1021
current,
packages/react-reconciler/src/ReactFiberHooks.js
+216
-52
@@ -2523,12 +2523,15 @@ function pushSimpleEffect(
2523
tag: HookFlags,
2524
inst: EffectInstance,
2525
create: () => (() => void) | void,
2526
- deps: Array<mixed> | void | null,
2526
+ createDeps: Array<mixed> | void | null,
2527
+ update?: ((resource: {...} | void | null) => void) | void,
2528
+ updateDeps?: Array<mixed> | void | null,
2529
+ destroy?: ((resource: {...} | void | null) => void) | void,
2530
): Effect {
2531
const effect: Effect = {
2532
tag,
2533
create,
2531
- deps,
2534
+ deps: createDeps,
2535
inst,
2536
// Circular
2537
next: (null: any),
@@ -2608,10 +2611,13 @@ function mountEffectImpl(
2611
fiberFlags: Flags,
2612
hookFlags: HookFlags,
2613
create: () => (() => void) | void,
2611
- deps: Array<mixed> | void | null,
2614
+ createDeps: Array<mixed> | void | null,
2615
+ update?: ((resource: {...} | void | null) => void) | void,
2616
+ updateDeps?: Array<mixed> | void | null,
2617
+ destroy?: ((resource: {...} | void | null) => void) | void,
2618
): void {
2619
const hook = mountWorkInProgressHook();
2614
- const nextDeps = deps === undefined ? null : deps;
2620
+ const nextDeps = createDeps === undefined ? null : createDeps;
2621
currentlyRenderingFiber.flags |= fiberFlags;
2622
hook.memoizedState = pushSimpleEffect(
2623
HookHasEffect | hookFlags,
@@ -2662,35 +2668,89 @@ function updateEffectImpl(
2668
}
2669
2670
function mountEffect(
2665
- create: () => (() => void) | void,
2666
- deps: Array<mixed> | void | null,
2671
+ create: (() => (() => void) | void) | (() => {...} | void | null),
2672
+ createDeps: Array<mixed> | void | null,
2673
+ update?: ((resource: {...} | void | null) => void) | void,
2674
+ updateDeps?: Array<mixed> | void | null,
2675
+ destroy?: ((resource: {...} | void | null) => void) | void,
2676
): void {
2677
if (
2678
__DEV__ &&
2679
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
2680
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
2681
) {
2673
- mountEffectImpl(
2674
- MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
2675
- HookPassive,
2676
- create,
2677
- deps,
2678
- );
2682
+ if (
2683
+ enableUseEffectCRUDOverload &&
2684
+ (typeof update === 'function' || typeof destroy === 'function')
2685
+ ) {
2686
+ mountResourceEffectImpl(
2687
+ MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
2688
+ HookPassive,
2689
+ create,
2690
+ createDeps,
2691
+ update,
2692
+ updateDeps,
2693
+ destroy,
2694
+ );
2695
+ } else {
2696
+ mountEffectImpl(
2697
+ MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
2698
+ HookPassive,
2699
+ // $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
2700
+ create,
2701
+ createDeps,
2702
+ );
2703
+ }
2704
} else {
2680
- mountEffectImpl(
2681
- PassiveEffect | PassiveStaticEffect,
2682
- HookPassive,
2683
- create,
2684
- deps,
2685
- );
2705
+ if (
2706
+ enableUseEffectCRUDOverload &&
2707
+ (typeof update === 'function' || typeof destroy === 'function')
2708
+ ) {
2709
+ mountResourceEffectImpl(
2710
+ PassiveEffect | PassiveStaticEffect,
2711
+ HookPassive,
2712
+ create,
2713
+ createDeps,
2714
+ update,
2715
+ updateDeps,
2716
+ destroy,
2717
+ );
2718
+ } else {
2719
+ mountEffectImpl(
2720
+ PassiveEffect | PassiveStaticEffect,
2721
+ HookPassive,
2722
+ // $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
2723
+ create,
2724
+ createDeps,
2725
+ );
2726
+ }
2727
}
2728
}
2729
2730
function updateEffect(
2690
- create: () => (() => void) | void,
2691
- deps: Array<mixed> | void | null,
2731
+ create: (() => (() => void) | void) | (() => {...} | void | null),
2732
+ createDeps: Array<mixed> | void | null,
2733
+ update?: ((resource: {...} | void | null) => void) | void,
2734
+ updateDeps?: Array<mixed> | void | null,
2735
+ destroy?: ((resource: {...} | void | null) => void) | void,
2736
): void {
2693
- updateEffectImpl(PassiveEffect, HookPassive, create, deps);
2737
+ if (
2738
+ enableUseEffectCRUDOverload &&
2739
+ (typeof update === 'function' || typeof destroy === 'function')
2740
+ ) {
2741
+ updateResourceEffectImpl(
2742
+ PassiveEffect,
2743
+ HookPassive,
2744
+ create,
2745
+ createDeps,
2746
+ update,
2747
+ updateDeps,
2748
+ destroy,
2749
+ );
2750
+ } else {
2751
+ // $FlowFixMe[incompatible-call] @poteto it's not possible to narrow `create` without calling it.
2752
+ updateEffectImpl(PassiveEffect, HookPassive, create, createDeps);
2753
+ }
2754
}
2755
2756
function mountResourceEffect(
@@ -2705,15 +2765,6 @@ function mountResourceEffect(
2765
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
2766
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
2767
) {
2708
- mountResourceEffectImpl(
2709
- MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
2710
- HookPassive,
2711
- create,
2712
- createDeps,
2713
- update,
2714
- updateDeps,
2715
- destroy,
2716
- );
2768
} else {
2769
mountResourceEffectImpl(
2770
PassiveEffect | PassiveStaticEffect,
@@ -4087,13 +4138,30 @@ if (__DEV__) {
4138
return readContext(context);
4139
},
4140
useEffect(
4090
- create: () => (() => void) | void,
4091
- deps: Array<mixed> | void | null,
4141
+ create: (() => (() => void) | void) | (() => {...} | void | null),
4142
+ createDeps: Array<mixed> | void | null,
4143
+ update?: ((resource: {...} | void | null) => void) | void,
4144
+ updateDeps?: Array<mixed> | void | null,
4145
+ destroy?: ((resource: {...} | void | null) => void) | void,
4146
): void {
4147
currentHookNameInDev = 'useEffect';
4148
mountHookTypesDev();
4095
- checkDepsAreArrayDev(deps);
4096
- return mountEffect(create, deps);
4149
+ if (
4150
+ enableUseEffectCRUDOverload &&
4151
+ (typeof update === 'function' || typeof destroy === 'function')
4152
+ ) {
4153
+ checkDepsAreNonEmptyArrayDev(updateDeps);
4154
+ return mountResourceEffect(
4155
+ create,
4156
+ createDeps,
4157
+ update,
4158
+ updateDeps,
4159
+ destroy,
4160
+ );
4161
+ } else {
4162
+ checkDepsAreArrayDev(createDeps);
4163
+ return mountEffect(create, createDeps);
4164
+ }
4165
},
4166
useImperativeHandle<T>(
4167
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4280,12 +4348,28 @@ if (__DEV__) {
4348
return readContext(context);
4349
},
4350
useEffect(
4283
- create: () => (() => void) | void,
4284
- deps: Array<mixed> | void | null,
4351
+ create: (() => (() => void) | void) | (() => {...} | void | null),
4352
+ createDeps: Array<mixed> | void | null,
4353
+ update?: ((resource: {...} | void | null) => void) | void,
4354
+ updateDeps?: Array<mixed> | void | null,
4355
+ destroy?: ((resource: {...} | void | null) => void) | void,
4356
): void {
4357
currentHookNameInDev = 'useEffect';
4358
updateHookTypesDev();
4288
- return mountEffect(create, deps);
4359
+ if (
4360
+ enableUseEffectCRUDOverload &&
4361
+ (typeof update === 'function' || typeof destroy === 'function')
4362
+ ) {
4363
+ return mountResourceEffect(
4364
+ create,
4365
+ createDeps,
4366
+ update,
4367
+ updateDeps,
4368
+ destroy,
4369
+ );
4370
+ } else {
4371
+ return mountEffect(create, createDeps);
4372
+ }
4373
},
4374
useImperativeHandle<T>(
4375
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4467,12 +4551,28 @@ if (__DEV__) {
4551
return readContext(context);
4552
},
4553
useEffect(
4470
- create: () => (() => void) | void,
4471
- deps: Array<mixed> | void | null,
4554
+ create: (() => (() => void) | void) | (() => {...} | void | null),
4555
+ createDeps: Array<mixed> | void | null,
4556
+ update?: ((resource: {...} | void | null) => void) | void,
4557
+ updateDeps?: Array<mixed> | void | null,
4558
+ destroy?: ((resource: {...} | void | null) => void) | void,
4559
): void {
4560
currentHookNameInDev = 'useEffect';
4561
updateHookTypesDev();
4475
- return updateEffect(create, deps);
4562
+ if (
4563
+ enableUseEffectCRUDOverload &&
4564
+ (typeof update === 'function' || typeof destroy === 'function')
4565
+ ) {
4566
+ return updateResourceEffect(
4567
+ create,
4568
+ createDeps,
4569
+ update,
4570
+ updateDeps,
4571
+ destroy,
4572
+ );
4573
+ } else {
4574
+ return updateEffect(create, createDeps);
4575
+ }
4576
},
4577
useImperativeHandle<T>(
4578
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4654,12 +4754,28 @@ if (__DEV__) {
4754
return readContext(context);
4755
},
4756
useEffect(
4657
- create: () => (() => void) | void,
4658
- deps: Array<mixed> | void | null,
4757
+ create: (() => (() => void) | void) | (() => {...} | void | null),
4758
+ createDeps: Array<mixed> | void | null,
4759
+ update?: ((resource: {...} | void | null) => void) | void,
4760
+ updateDeps?: Array<mixed> | void | null,
4761
+ destroy?: ((resource: {...} | void | null) => void) | void,
4762
): void {
4763
currentHookNameInDev = 'useEffect';
4764
updateHookTypesDev();
4662
- return updateEffect(create, deps);
4765
+ if (
4766
+ enableUseEffectCRUDOverload &&
4767
+ (typeof update === 'function' || typeof destroy === 'function')
4768
+ ) {
4769
+ return updateResourceEffect(
4770
+ create,
4771
+ createDeps,
4772
+ update,
4773
+ updateDeps,
4774
+ destroy,
4775
+ );
4776
+ } else {
4777
+ return updateEffect(create, createDeps);
4778
+ }
4779
},
4780
useImperativeHandle<T>(
4781
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -4847,13 +4963,29 @@ if (__DEV__) {
4963
return readContext(context);
4964
},
4965
useEffect(
4850
- create: () => (() => void) | void,
4851
- deps: Array<mixed> | void | null,
4966
+ create: (() => (() => void) | void) | (() => {...} | void | null),
4967
+ createDeps: Array<mixed> | void | null,
4968
+ update?: ((resource: {...} | void | null) => void) | void,
4969
+ updateDeps?: Array<mixed> | void | null,
4970
+ destroy?: ((resource: {...} | void | null) => void) | void,
4971
): void {
4972
currentHookNameInDev = 'useEffect';
4973
warnInvalidHookAccess();
4974
mountHookTypesDev();
4856
- return mountEffect(create, deps);
4975
+ if (
4976
+ enableUseEffectCRUDOverload &&
4977
+ (typeof update === 'function' || typeof destroy === 'function')
4978
+ ) {
4979
+ return mountResourceEffect(
4980
+ create,
4981
+ createDeps,
4982
+ update,
4983
+ updateDeps,
4984
+ destroy,
4985
+ );
4986
+ } else {
4987
+ return mountEffect(create, createDeps);
4988
+ }
4989
},
4990
useImperativeHandle<T>(
4991
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -5060,13 +5192,29 @@ if (__DEV__) {
5192
return readContext(context);
5193
},
5194
useEffect(
5063
- create: () => (() => void) | void,
5064
- deps: Array<mixed> | void | null,
5195
+ create: (() => (() => void) | void) | (() => {...} | void | null),
5196
+ createDeps: Array<mixed> | void | null,
5197
+ update?: ((resource: {...} | void | null) => void) | void,
5198
+ updateDeps?: Array<mixed> | void | null,
5199
+ destroy?: ((resource: {...} | void | null) => void) | void,
5200
): void {
5201
currentHookNameInDev = 'useEffect';
5202
warnInvalidHookAccess();
5203
updateHookTypesDev();
5069
- return updateEffect(create, deps);
5204
+ if (
5205
+ enableUseEffectCRUDOverload &&
5206
+ (typeof update === 'function' || typeof destroy === 'function')
5207
+ ) {
5208
+ return updateResourceEffect(
5209
+ create,
5210
+ createDeps,
5211
+ update,
5212
+ updateDeps,
5213
+ destroy,
5214
+ );
5215
+ } else {
5216
+ return updateEffect(create, createDeps);
5217
+ }
5218
},
5219
useImperativeHandle<T>(
5220
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
@@ -5273,13 +5421,29 @@ if (__DEV__) {
5421
return readContext(context);
5422
},
5423
useEffect(
5276
- create: () => (() => void) | void,
5277
- deps: Array<mixed> | void | null,
5424
+ create: (() => (() => void) | void) | (() => {...} | void | null),
5425
+ createDeps: Array<mixed> | void | null,
5426
+ update?: ((resource: {...} | void | null) => void) | void,
5427
+ updateDeps?: Array<mixed> | void | null,
5428
+ destroy?: ((resource: {...} | void | null) => void) | void,
5429
): void {
5430
currentHookNameInDev = 'useEffect';
5431
warnInvalidHookAccess();
5432
updateHookTypesDev();
5282
- return updateEffect(create, deps);
5433
+ if (
5434
+ enableUseEffectCRUDOverload &&
5435
+ (typeof update === 'function' || typeof destroy === 'function')
5436
+ ) {
5437
+ return updateResourceEffect(
5438
+ create,
5439
+ createDeps,
5440
+ update,
5441
+ updateDeps,
5442
+ destroy,
5443
+ );
5444
+ } else {
5445
+ return updateEffect(create, createDeps);
5446
+ }
5447
},
5448
useImperativeHandle<T>(
5449
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
packages/react-reconciler/src/ReactInternalTypes.js
+5
-2
@@ -391,8 +391,11 @@ export type Dispatcher = {
391
useContext<T>(context: ReactContext<T>): T,
392
useRef<T>(initialValue: T): {current: T},
393
useEffect(
394
- create: () => (() => void) | void,
395
- deps: Array<mixed> | void | null,
394
+ create: (() => (() => void) | void) | (() => {...} | void | null),
395
+ createDeps: Array<mixed> | void | null,
396
+ update?: ((resource: {...} | void | null) => void) | void,
397
+ updateDeps?: Array<mixed> | void | null,
398
+ destroy?: ((resource: {...} | void | null) => void) | void,
399
): void,
400
// TODO: Non-nullable once `enableUseEffectEventHook` is on everywhere.
401
useEffectEvent?: <Args, F: (...Array<Args>) => mixed>(callback: F) => F,
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+34
-36
@@ -41,7 +41,6 @@ let waitFor;
41
let waitForThrow;
42
let waitForPaint;
43
let assertLog;
44
-let useResourceEffect;
44
let assertConsoleErrorDev;
45
46
describe('ReactHooksWithNoopRenderer', () => {
@@ -70,7 +69,6 @@ describe('ReactHooksWithNoopRenderer', () => {
69
useDeferredValue = React.useDeferredValue;
70
Suspense = React.Suspense;
71
Activity = React.unstable_Activity;
73
- useResourceEffect = React.experimental_useResourceEffect;
72
ContinuousEventPriority =
73
require('react-reconciler/constants').ContinuousEventPriority;
74
if (gate(flags => flags.enableSuspenseList)) {
@@ -3312,7 +3310,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3310
});
3311
3312
// @gate enableUseEffectCRUDOverload
3315
- describe('useResourceEffect', () => {
3313
+ describe('useEffect CRUD overload', () => {
3314
class Resource {
3315
isDeleted: false;
3316
id: string;
@@ -3335,34 +3333,34 @@ describe('ReactHooksWithNoopRenderer', () => {
3333
3334
// @gate !enableUseEffectCRUDOverload
3335
it('is null when flag is disabled', async () => {
3338
- expect(useResourceEffect).toBeUndefined();
3339
- });
3340
-
3341
- // @gate enableUseEffectCRUDOverload
3342
- it('validates create return value', async () => {
3336
function App({id}) {
3344
- useResourceEffect(() => {
3345
- Scheduler.log(`create(${id})`);
3346
- }, [id]);
3337
+ useEffect(
3338
+ () => {
3339
+ Scheduler.log(`create(${id})`);
3340
+ return {};
3341
+ },
3342
+ [id],
3343
+ () => {
3344
+ Scheduler.log('update');
3345
+ },
3346
+ [],
3347
+ );
3348
return null;
3349
}
3350
3350
- await act(() => {
3351
- ReactNoop.render(<App id={1} />);
3352
- });
3353
- assertConsoleErrorDev(
3354
- [
3355
- 'useResourceEffect must provide a callback which returns a resource. ' +
3356
- 'If a managed resource is not needed here, use useEffect. Received undefined',
3357
- ],
3358
- {withoutStack: true},
3351
+ await expect(async () => {
3352
+ await act(() => {
3353
+ ReactNoop.render(<App id={1} />);
3354
+ });
3355
+ }).rejects.toThrow(
3356
+ 'useEffect CRUD overload is not enabled in this build of React.',
3357
);
3358
});
3359
3360
// @gate enableUseEffectCRUDOverload
3361
it('validates non-empty update deps', async () => {
3362
function App({id}) {
3365
- useResourceEffect(
3363
+ useEffect(
3364
() => {
3365
Scheduler.log(`create(${id})`);
3366
return {};
@@ -3380,7 +3378,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3378
ReactNoop.render(<App id={1} />);
3379
});
3380
assertConsoleErrorDev([
3383
- 'useResourceEffect received a dependency array with no dependencies. ' +
3381
+ 'useEffect received a dependency array with no dependencies. ' +
3382
'When specified, the dependency array must have at least one dependency.\n' +
3383
' in App (at **)',
3384
]);
@@ -3392,7 +3390,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3390
const opts = useMemo(() => {
3391
return {username};
3392
}, [username]);
3395
- useResourceEffect(
3393
+ useEffect(
3394
() => {
3395
const resource = new Resource(id, opts);
3396
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3449,7 +3447,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3447
const opts = useMemo(() => {
3448
return {username};
3449
}, [username]);
3452
- useResourceEffect(
3450
+ useEffect(
3451
() => {
3452
const resource = new Resource(id, opts);
3453
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3486,7 +3484,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3484
const opts = useMemo(() => {
3485
return {username};
3486
}, [username]);
3489
- useResourceEffect(
3487
+ useEffect(
3488
() => {
3489
const resource = new Resource(id, opts);
3490
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3524,9 +3522,9 @@ describe('ReactHooksWithNoopRenderer', () => {
3522
});
3523
3524
// @gate enableUseEffectCRUDOverload
3527
- it('does not unmount previous useResourceEffect between updates', async () => {
3525
+ it('does not unmount previous useEffect between updates', async () => {
3526
function App({id}) {
3529
- useResourceEffect(
3527
+ useEffect(
3528
() => {
3529
const resource = new Resource(id);
3530
Scheduler.log(`create(${resource.id})`);
@@ -3565,7 +3563,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3563
// @gate enableUseEffectCRUDOverload
3564
it('unmounts only on deletion', async () => {
3565
function App({id}) {
3568
- useResourceEffect(
3566
+ useEffect(
3567
() => {
3568
const resource = new Resource(id);
3569
Scheduler.log(`create(${resource.id})`);
@@ -3605,7 +3603,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3603
const opts = useMemo(() => {
3604
return {username};
3605
}, [username]);
3608
- useResourceEffect(
3606
+ useEffect(
3607
() => {
3608
const resource = new Resource(id, opts);
3609
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3653,7 +3651,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3651
// @gate enableUseEffectCRUDOverload
3652
it('handles errors in create on mount', async () => {
3653
function App({id}) {
3656
- useResourceEffect(
3654
+ useEffect(
3655
() => {
3656
Scheduler.log(`Mount A [${id}]`);
3657
return {};
@@ -3665,7 +3663,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3663
Scheduler.log(`Unmount A [${id}]`);
3664
},
3665
);
3668
- useResourceEffect(
3666
+ useEffect(
3667
() => {
3668
Scheduler.log('Oops!');
3669
throw new Error('Oops!');
@@ -3703,7 +3701,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3701
// @gate enableUseEffectCRUDOverload
3702
it('handles errors in create on update', async () => {
3703
function App({id}) {
3706
- useResourceEffect(
3704
+ useEffect(
3705
() => {
3706
Scheduler.log(`Mount A [${id}]`);
3707
return {};
@@ -3750,7 +3748,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3748
const opts = useMemo(() => {
3749
return {username};
3750
}, [username]);
3753
- useResourceEffect(
3751
+ useEffect(
3752
() => {
3753
const resource = new Resource(id, opts);
3754
Scheduler.log(`Mount A [${id}, ${resource.opts.username}]`);
@@ -3806,7 +3804,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3804
const opts = useMemo(() => {
3805
return {username};
3806
}, [username]);
3809
- useResourceEffect(
3807
+ useEffect(
3808
() => {
3809
const resource = new Resource(id, opts);
3810
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -3885,7 +3883,7 @@ describe('ReactHooksWithNoopRenderer', () => {
3883
const opts = useMemo(() => {
3884
return {username};
3885
}, [username]);
3888
- useResourceEffect(
3886
+ useEffect(
3887
() => {
3888
const resource = new Resource(id, opts);
3889
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
@@ -4003,7 +4001,7 @@ describe('ReactHooksWithNoopRenderer', () => {
4001
useEffect(() => {
4002
Scheduler.log(`useEffect(${count})`);
4003
}, [count]);
4006
- useResourceEffect(
4004
+ useEffect(
4005
() => {
4006
const resource = new Resource(id, opts);
4007
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
packages/react/src/ReactHooks.js
+23
-3
@@ -87,11 +87,31 @@ export function useRef<T>(initialValue: T): {current: T} {
87
}
88
89
export function useEffect(
90
- create: () => (() => void) | void,
91
- deps: Array<mixed> | void | null,
90
+ create: (() => (() => void) | void) | (() => {...} | void | null),
91
+ createDeps: Array<mixed> | void | null,
92
+ update?: ((resource: {...} | void | null) => void) | void,
93
+ updateDeps?: Array<mixed> | void | null,
94
+ destroy?: ((resource: {...} | void | null) => void) | void,
95
): void {
96
const dispatcher = resolveDispatcher();
94
- return dispatcher.useEffect(create, deps);
97
+ if (
98
+ enableUseEffectCRUDOverload &&
99
+ (typeof update === 'function' || typeof destroy === 'function')
100
+ ) {
101
+ // $FlowFixMe[not-a-function] This is unstable, thus optional
102
+ return dispatcher.useEffect(
103
+ create,
104
+ createDeps,
105
+ update,
106
+ updateDeps,
107
+ destroy,
108
+ );
109
+ } else if (typeof update === 'function') {
110
+ throw new Error(
111
+ 'useEffect CRUD overload is not enabled in this build of React.',
112
+ );
113
+ }
114
+ return dispatcher.useEffect(create, createDeps);
115
}
116
117
export function useInsertionEffect(
scripts/error-codes/codes.json
+2
-1
@@ -530,5 +530,6 @@
530
"542": "Suspense Exception: This is not a real error! It's an implementation detail of `useActionState` to interrupt the current render. You must either rethrow it immediately, or move the `useActionState` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary.",
531
"543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React.",
532
"544": "Found a pair with an auto name. This is a bug in React.",
533
- "545": "The %s tag may only be rendered once."
533
+ "545": "The %s tag may only be rendered once.",
534
+ "546": "useEffect CRUD overload is not enabled in this build of React."
535
}