@samitouri / QOS-React / commits / 2c5fd26c07

[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 }