mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[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.
This commit is contained in:
@@ -14,6 +14,11 @@ import type {CapturedValue} from './ReactCapturedValue';
|
||||
|
||||
import {isRendering, setIsRendering} from './ReactCurrentFiber';
|
||||
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
ResourceEffectIdentityKind,
|
||||
ResourceEffectUpdateKind,
|
||||
} from './ReactFiberHooks';
|
||||
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
|
||||
// TODO: Consider marking the whole bundle instead of these boundaries.
|
||||
@@ -176,12 +181,54 @@ export const callComponentWillUnmountInDEV: (
|
||||
: (null: any);
|
||||
|
||||
const callCreate = {
|
||||
'react-stack-bottom-frame': function (effect: Effect): (() => void) | void {
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
const destroy = create();
|
||||
inst.destroy = destroy;
|
||||
return destroy;
|
||||
'react-stack-bottom-frame': function (
|
||||
effect: Effect,
|
||||
): (() => void) | mixed | void {
|
||||
if (!enableUseResourceEffectHook) {
|
||||
if (effect.resourceKind != null) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
|
||||
'got %s',
|
||||
effect.resourceKind,
|
||||
);
|
||||
}
|
||||
}
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
// $FlowFixMe[not-a-function] (@poteto)
|
||||
const destroy = create();
|
||||
// $FlowFixMe[incompatible-type] (@poteto)
|
||||
inst.destroy = destroy;
|
||||
return destroy;
|
||||
} else {
|
||||
if (effect.resourceKind == null) {
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
const destroy = create();
|
||||
inst.destroy = destroy;
|
||||
return destroy;
|
||||
}
|
||||
switch (effect.resourceKind) {
|
||||
case ResourceEffectIdentityKind: {
|
||||
return effect.create();
|
||||
}
|
||||
case ResourceEffectUpdateKind: {
|
||||
if (typeof effect.update === 'function') {
|
||||
effect.update(effect.inst.resource);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Unhandled Effect kind %s. This is a bug in React.',
|
||||
effect.kind,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
enableProfilerNestedUpdatePhase,
|
||||
enableSchedulingProfiler,
|
||||
enableScopeAPI,
|
||||
enableUseResourceEffectHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
ClassComponent,
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
Layout as HookLayout,
|
||||
Insertion as HookInsertion,
|
||||
Passive as HookPassive,
|
||||
HasEffect as HookHasEffect,
|
||||
} from './ReactHookEffectTags';
|
||||
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
|
||||
import {
|
||||
@@ -70,6 +72,10 @@ import {
|
||||
} from './ReactFiberCallUserSpace';
|
||||
|
||||
import {runWithFiberInDEV} from './ReactCurrentFiber';
|
||||
import {
|
||||
ResourceEffectIdentityKind,
|
||||
ResourceEffectUpdateKind,
|
||||
} from './ReactFiberHooks';
|
||||
|
||||
function shouldProfile(current: Fiber): boolean {
|
||||
return (
|
||||
@@ -146,19 +152,90 @@ export function commitHookEffectListMount(
|
||||
|
||||
// Mount
|
||||
let destroy;
|
||||
if (enableUseResourceEffectHook) {
|
||||
if (effect.resourceKind === ResourceEffectIdentityKind) {
|
||||
if (__DEV__) {
|
||||
effect.inst.resource = runWithFiberInDEV(
|
||||
finishedWork,
|
||||
callCreateInDEV,
|
||||
effect,
|
||||
);
|
||||
if (effect.inst.resource == null) {
|
||||
console.error(
|
||||
'useResourceEffect must provide a callback which returns a resource. ' +
|
||||
'If a managed resource is not needed here, use useEffect. Received %s',
|
||||
effect.inst.resource,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
effect.inst.resource = effect.create();
|
||||
}
|
||||
destroy = effect.inst.destroy;
|
||||
}
|
||||
if (effect.resourceKind === ResourceEffectUpdateKind) {
|
||||
if (
|
||||
// We don't want to fire updates on remount during Activity
|
||||
(flags & HookHasEffect) > 0 &&
|
||||
typeof effect.update === 'function' &&
|
||||
effect.inst.resource != null
|
||||
) {
|
||||
// TODO(@poteto) what about multiple updates?
|
||||
if (__DEV__) {
|
||||
runWithFiberInDEV(finishedWork, callCreateInDEV, effect);
|
||||
} else {
|
||||
effect.update(effect.inst.resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
if ((flags & HookInsertion) !== NoHookEffect) {
|
||||
setIsRunningInsertionEffect(true);
|
||||
}
|
||||
destroy = runWithFiberInDEV(finishedWork, callCreateInDEV, effect);
|
||||
if (enableUseResourceEffectHook) {
|
||||
if (effect.resourceKind == null) {
|
||||
destroy = runWithFiberInDEV(
|
||||
finishedWork,
|
||||
callCreateInDEV,
|
||||
effect,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
destroy = runWithFiberInDEV(
|
||||
finishedWork,
|
||||
callCreateInDEV,
|
||||
effect,
|
||||
);
|
||||
}
|
||||
if ((flags & HookInsertion) !== NoHookEffect) {
|
||||
setIsRunningInsertionEffect(false);
|
||||
}
|
||||
} else {
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
destroy = create();
|
||||
inst.destroy = destroy;
|
||||
if (enableUseResourceEffectHook) {
|
||||
if (effect.resourceKind == null) {
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
destroy = create();
|
||||
inst.destroy = destroy;
|
||||
}
|
||||
} else {
|
||||
if (effect.resourceKind != null) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Expected only SimpleEffects when enableUseResourceEffectHook is disabled, ' +
|
||||
'got %s',
|
||||
effect.resourceKind,
|
||||
);
|
||||
}
|
||||
}
|
||||
const create = effect.create;
|
||||
const inst = effect.inst;
|
||||
// $FlowFixMe[incompatible-type] (@poteto)
|
||||
// $FlowFixMe[not-a-function] (@poteto)
|
||||
destroy = create();
|
||||
// $FlowFixMe[incompatible-type] (@poteto)
|
||||
inst.destroy = destroy;
|
||||
}
|
||||
}
|
||||
|
||||
if (enableSchedulingProfiler) {
|
||||
@@ -176,6 +253,11 @@ export function commitHookEffectListMount(
|
||||
hookName = 'useLayoutEffect';
|
||||
} else if ((effect.tag & HookInsertion) !== NoFlags) {
|
||||
hookName = 'useInsertionEffect';
|
||||
} else if (
|
||||
enableUseResourceEffectHook &&
|
||||
effect.resourceKind != null
|
||||
) {
|
||||
hookName = 'useResourceEffect';
|
||||
} else {
|
||||
hookName = 'useEffect';
|
||||
}
|
||||
@@ -202,6 +284,7 @@ export function commitHookEffectListMount(
|
||||
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
|
||||
'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching';
|
||||
} else {
|
||||
// $FlowFixMe[unsafe-addition] (@poteto)
|
||||
addendum = ' You returned: ' + destroy;
|
||||
}
|
||||
runWithFiberInDEV(
|
||||
@@ -246,7 +329,13 @@ export function commitHookEffectListUnmount(
|
||||
const inst = effect.inst;
|
||||
const destroy = inst.destroy;
|
||||
if (destroy !== undefined) {
|
||||
inst.destroy = undefined;
|
||||
if (enableUseResourceEffectHook) {
|
||||
if (effect.resourceKind == null) {
|
||||
inst.destroy = undefined;
|
||||
}
|
||||
} else {
|
||||
inst.destroy = undefined;
|
||||
}
|
||||
if (enableSchedulingProfiler) {
|
||||
if ((flags & HookPassive) !== NoHookEffect) {
|
||||
markComponentPassiveEffectUnmountStarted(finishedWork);
|
||||
@@ -260,7 +349,41 @@ export function commitHookEffectListUnmount(
|
||||
setIsRunningInsertionEffect(true);
|
||||
}
|
||||
}
|
||||
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
|
||||
if (enableUseResourceEffectHook) {
|
||||
if (
|
||||
effect.resourceKind === ResourceEffectIdentityKind &&
|
||||
effect.inst.resource != null
|
||||
) {
|
||||
safelyCallDestroyWithResource(
|
||||
finishedWork,
|
||||
nearestMountedAncestor,
|
||||
destroy,
|
||||
effect.inst.resource,
|
||||
);
|
||||
if (effect.next.resourceKind === ResourceEffectUpdateKind) {
|
||||
// $FlowFixMe[prop-missing] (@poteto)
|
||||
effect.next.update = undefined;
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Expected a ResourceEffectUpdateKind to follow ResourceEffectIdentityKind, ' +
|
||||
'got %s. This is a bug in React.',
|
||||
effect.next.resourceKind,
|
||||
);
|
||||
}
|
||||
}
|
||||
effect.inst.resource = null;
|
||||
}
|
||||
if (effect.resourceKind == null) {
|
||||
safelyCallDestroy(
|
||||
finishedWork,
|
||||
nearestMountedAncestor,
|
||||
destroy,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
|
||||
}
|
||||
if (__DEV__) {
|
||||
if ((flags & HookInsertion) !== NoHookEffect) {
|
||||
setIsRunningInsertionEffect(false);
|
||||
@@ -895,6 +1018,30 @@ function safelyCallDestroy(
|
||||
}
|
||||
}
|
||||
|
||||
function safelyCallDestroyWithResource(
|
||||
current: Fiber,
|
||||
nearestMountedAncestor: Fiber | null,
|
||||
destroy: mixed => void,
|
||||
resource: mixed,
|
||||
) {
|
||||
const destroy_ = resource == null ? destroy : destroy.bind(null, resource);
|
||||
if (__DEV__) {
|
||||
runWithFiberInDEV(
|
||||
current,
|
||||
callDestroyInDEV,
|
||||
current,
|
||||
nearestMountedAncestor,
|
||||
destroy_,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
destroy_();
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function commitProfiler(
|
||||
finishedWork: Fiber,
|
||||
current: Fiber | null,
|
||||
|
||||
426
packages/react-reconciler/src/ReactFiberHooks.js
vendored
426
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -48,6 +48,7 @@ import {
|
||||
disableLegacyMode,
|
||||
enableNoCloningMemoCache,
|
||||
enableContextProfiling,
|
||||
enableUseResourceEffectHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
REACT_CONTEXT_TYPE,
|
||||
@@ -217,15 +218,40 @@ export type Hook = {
|
||||
// the additional memory and we can follow up with performance
|
||||
// optimizations later.
|
||||
type EffectInstance = {
|
||||
destroy: void | (() => void),
|
||||
resource: mixed,
|
||||
destroy: void | (() => void) | ((resource: mixed) => void),
|
||||
};
|
||||
|
||||
export type Effect = SimpleEffect;
|
||||
export const ResourceEffectIdentityKind: 0 = 0;
|
||||
export const ResourceEffectUpdateKind: 1 = 1;
|
||||
export type EffectKind =
|
||||
| typeof ResourceEffectIdentityKind
|
||||
| typeof ResourceEffectUpdateKind;
|
||||
export type Effect =
|
||||
| SimpleEffect
|
||||
| ResourceEffectIdentity
|
||||
| ResourceEffectUpdate;
|
||||
export type SimpleEffect = {
|
||||
tag: HookFlags,
|
||||
create: () => (() => void) | void,
|
||||
inst: EffectInstance,
|
||||
deps: Array<mixed> | null,
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
next: Effect,
|
||||
};
|
||||
export type ResourceEffectIdentity = {
|
||||
resourceKind: typeof ResourceEffectIdentityKind,
|
||||
tag: HookFlags,
|
||||
inst: EffectInstance,
|
||||
create: () => mixed,
|
||||
deps: Array<mixed> | void | null,
|
||||
next: Effect,
|
||||
};
|
||||
export type ResourceEffectUpdate = {
|
||||
resourceKind: typeof ResourceEffectUpdateKind,
|
||||
tag: HookFlags,
|
||||
inst: EffectInstance,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
next: Effect,
|
||||
};
|
||||
|
||||
@@ -350,6 +376,23 @@ function checkDepsAreArrayDev(deps: mixed): void {
|
||||
}
|
||||
}
|
||||
|
||||
function checkDepsAreNonEmptyArrayDev(deps: mixed): void {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
deps !== undefined &&
|
||||
deps !== null &&
|
||||
isArray(deps) &&
|
||||
deps.length === 0
|
||||
) {
|
||||
console.error(
|
||||
'%s received a dependency array with no dependencies. When ' +
|
||||
'specified, the dependency array must have at least one dependency.',
|
||||
currentHookNameInDev,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function warnOnHookMismatchInDev(currentHookName: HookType): void {
|
||||
if (__DEV__) {
|
||||
const componentName = getComponentNameFromFiber(currentlyRenderingFiber);
|
||||
@@ -1721,10 +1764,10 @@ function mountSyncExternalStore<T>(
|
||||
// directly, without storing any additional state. For the same reason, we
|
||||
// don't need to set a static flag, either.
|
||||
fiber.flags |= PassiveEffect;
|
||||
pushEffect(
|
||||
pushSimpleEffect(
|
||||
HookHasEffect | HookPassive,
|
||||
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
|
||||
createEffectInstance(),
|
||||
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -1791,10 +1834,10 @@ function updateSyncExternalStore<T>(
|
||||
workInProgressHook.memoizedState.tag & HookHasEffect)
|
||||
) {
|
||||
fiber.flags |= PassiveEffect;
|
||||
pushEffect(
|
||||
pushSimpleEffect(
|
||||
HookHasEffect | HookPassive,
|
||||
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
|
||||
createEffectInstance(),
|
||||
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -2465,10 +2508,10 @@ function updateActionStateImpl<S, P>(
|
||||
const prevAction = actionQueueHook.memoizedState;
|
||||
if (action !== prevAction) {
|
||||
currentlyRenderingFiber.flags |= PassiveEffect;
|
||||
pushEffect(
|
||||
pushSimpleEffect(
|
||||
HookHasEffect | HookPassive,
|
||||
actionStateActionEffect.bind(null, actionQueue, action),
|
||||
createEffectInstance(),
|
||||
actionStateActionEffect.bind(null, actionQueue, action),
|
||||
null,
|
||||
);
|
||||
}
|
||||
@@ -2525,17 +2568,53 @@ function rerenderActionState<S, P>(
|
||||
return [state, dispatch, false];
|
||||
}
|
||||
|
||||
function pushEffect(
|
||||
function pushSimpleEffect(
|
||||
tag: HookFlags,
|
||||
create: () => (() => void) | void,
|
||||
inst: EffectInstance,
|
||||
deps: Array<mixed> | null,
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
): Effect {
|
||||
const effect: Effect = {
|
||||
tag,
|
||||
create,
|
||||
inst,
|
||||
deps,
|
||||
inst,
|
||||
// Circular
|
||||
next: (null: any),
|
||||
};
|
||||
return pushEffectImpl(effect);
|
||||
}
|
||||
|
||||
function pushResourceEffectIdentity(
|
||||
tag: HookFlags,
|
||||
inst: EffectInstance,
|
||||
create: () => mixed,
|
||||
deps: Array<mixed> | void | null,
|
||||
): Effect {
|
||||
const effect: ResourceEffectIdentity = {
|
||||
resourceKind: ResourceEffectIdentityKind,
|
||||
tag,
|
||||
create,
|
||||
deps,
|
||||
inst,
|
||||
// Circular
|
||||
next: (null: any),
|
||||
};
|
||||
return pushEffectImpl(effect);
|
||||
}
|
||||
|
||||
function pushResourceEffectUpdate(
|
||||
tag: HookFlags,
|
||||
inst: EffectInstance,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
): Effect {
|
||||
const effect: ResourceEffectUpdate = {
|
||||
resourceKind: ResourceEffectUpdateKind,
|
||||
tag,
|
||||
update,
|
||||
deps,
|
||||
inst,
|
||||
// Circular
|
||||
next: (null: any),
|
||||
};
|
||||
@@ -2562,7 +2641,7 @@ function pushEffectImpl(effect: Effect): Effect {
|
||||
}
|
||||
|
||||
function createEffectInstance(): EffectInstance {
|
||||
return {destroy: undefined};
|
||||
return {destroy: undefined, resource: undefined};
|
||||
}
|
||||
|
||||
function mountRef<T>(initialValue: T): {current: T} {
|
||||
@@ -2586,10 +2665,10 @@ function mountEffectImpl(
|
||||
const hook = mountWorkInProgressHook();
|
||||
const nextDeps = deps === undefined ? null : deps;
|
||||
currentlyRenderingFiber.flags |= fiberFlags;
|
||||
hook.memoizedState = pushEffect(
|
||||
hook.memoizedState = pushSimpleEffect(
|
||||
HookHasEffect | hookFlags,
|
||||
create,
|
||||
createEffectInstance(),
|
||||
create,
|
||||
nextDeps,
|
||||
);
|
||||
}
|
||||
@@ -2611,8 +2690,14 @@ function updateEffectImpl(
|
||||
if (nextDeps !== null) {
|
||||
const prevEffect: Effect = currentHook.memoizedState;
|
||||
const prevDeps = prevEffect.deps;
|
||||
// $FlowFixMe[incompatible-call] (@poteto)
|
||||
if (areHookInputsEqual(nextDeps, prevDeps)) {
|
||||
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
|
||||
hook.memoizedState = pushSimpleEffect(
|
||||
hookFlags,
|
||||
inst,
|
||||
create,
|
||||
nextDeps,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2620,10 +2705,10 @@ function updateEffectImpl(
|
||||
|
||||
currentlyRenderingFiber.flags |= fiberFlags;
|
||||
|
||||
hook.memoizedState = pushEffect(
|
||||
hook.memoizedState = pushSimpleEffect(
|
||||
HookHasEffect | hookFlags,
|
||||
create,
|
||||
inst,
|
||||
create,
|
||||
nextDeps,
|
||||
);
|
||||
}
|
||||
@@ -2660,6 +2745,149 @@ function updateEffect(
|
||||
updateEffectImpl(PassiveEffect, HookPassive, create, deps);
|
||||
}
|
||||
|
||||
function mountResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
|
||||
(currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
|
||||
) {
|
||||
mountResourceEffectImpl(
|
||||
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
|
||||
HookPassive,
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
} else {
|
||||
mountResourceEffectImpl(
|
||||
PassiveEffect | PassiveStaticEffect,
|
||||
HookPassive,
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mountResourceEffectImpl(
|
||||
fiberFlags: Flags,
|
||||
hookFlags: HookFlags,
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
const hook = mountWorkInProgressHook();
|
||||
currentlyRenderingFiber.flags |= fiberFlags;
|
||||
const inst = createEffectInstance();
|
||||
inst.destroy = destroy;
|
||||
hook.memoizedState = pushResourceEffectIdentity(
|
||||
HookHasEffect | hookFlags,
|
||||
inst,
|
||||
create,
|
||||
createDeps,
|
||||
);
|
||||
hook.memoizedState = pushResourceEffectUpdate(
|
||||
hookFlags,
|
||||
inst,
|
||||
update,
|
||||
updateDeps,
|
||||
);
|
||||
}
|
||||
|
||||
function updateResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
updateResourceEffectImpl(
|
||||
PassiveEffect,
|
||||
HookPassive,
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
}
|
||||
|
||||
function updateResourceEffectImpl(
|
||||
fiberFlags: Flags,
|
||||
hookFlags: HookFlags,
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const effect: Effect = hook.memoizedState;
|
||||
const inst = effect.inst;
|
||||
inst.destroy = destroy;
|
||||
|
||||
const nextCreateDeps = createDeps === undefined ? null : createDeps;
|
||||
const nextUpdateDeps = updateDeps === undefined ? null : updateDeps;
|
||||
let isCreateDepsSame: boolean;
|
||||
let isUpdateDepsSame: boolean;
|
||||
|
||||
if (currentHook !== null) {
|
||||
const prevEffect: Effect = currentHook.memoizedState;
|
||||
if (nextCreateDeps !== null) {
|
||||
let prevCreateDeps;
|
||||
// Seems sketchy but in practice we always push an Identity and an Update together. For safety
|
||||
// we error in DEV if this does not hold true.
|
||||
if (prevEffect.resourceKind === ResourceEffectUpdateKind) {
|
||||
prevCreateDeps =
|
||||
prevEffect.next.deps != null ? prevEffect.next.deps : null;
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Expected a ResourceEffectUpdateKind to be pushed together with ' +
|
||||
'ResourceEffectIdentityKind, got %s. This is a bug in React.',
|
||||
prevEffect.resourceKind,
|
||||
);
|
||||
}
|
||||
prevCreateDeps = prevEffect.deps != null ? prevEffect.deps : null;
|
||||
}
|
||||
isCreateDepsSame = areHookInputsEqual(nextCreateDeps, prevCreateDeps);
|
||||
}
|
||||
if (nextUpdateDeps !== null) {
|
||||
const prevUpdateDeps = prevEffect.deps != null ? prevEffect.deps : null;
|
||||
isUpdateDepsSame = areHookInputsEqual(nextUpdateDeps, prevUpdateDeps);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(isCreateDepsSame && isUpdateDepsSame)) {
|
||||
currentlyRenderingFiber.flags |= fiberFlags;
|
||||
}
|
||||
|
||||
hook.memoizedState = pushResourceEffectIdentity(
|
||||
isCreateDepsSame ? hookFlags : HookHasEffect | hookFlags,
|
||||
inst,
|
||||
create,
|
||||
nextCreateDeps,
|
||||
);
|
||||
hook.memoizedState = pushResourceEffectUpdate(
|
||||
isUpdateDepsSame ? hookFlags : HookHasEffect | hookFlags,
|
||||
inst,
|
||||
update,
|
||||
nextUpdateDeps,
|
||||
);
|
||||
}
|
||||
|
||||
function useEffectEventImpl<Args, Return, F: (...Array<Args>) => Return>(
|
||||
payload: EventFunctionPayload<Args, Return, F>,
|
||||
) {
|
||||
@@ -3810,6 +4038,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useResourceEffect = throwInvalidHookError;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
|
||||
throwInvalidHookError;
|
||||
@@ -3853,6 +4084,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useResourceEffect = mountResourceEffect;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -3896,6 +4130,10 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useResourceEffect =
|
||||
updateResourceEffect;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -3939,6 +4177,10 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useResourceEffect =
|
||||
updateResourceEffect;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4129,6 +4371,27 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
): void {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
mountHookTypesDev();
|
||||
checkDepsAreNonEmptyArrayDev(updateDeps);
|
||||
return mountResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4321,6 +4584,26 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
): void {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
updateHookTypesDev();
|
||||
return mountResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4512,6 +4795,26 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
updateHookTypesDev();
|
||||
return updateResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4703,6 +5006,26 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
updateHookTypesDev();
|
||||
return updateResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4918,6 +5241,27 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (InvalidNestedHooksDispatcherOnMountInDEV) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
): void {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -5136,6 +5480,27 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -5354,6 +5719,27 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableUseResourceEffectHook) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useResourceEffect =
|
||||
function useResourceEffect(
|
||||
create: () => mixed,
|
||||
createDeps: Array<mixed> | void | null,
|
||||
update: ((resource: mixed) => void) | void,
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
) {
|
||||
currentHookNameInDev = 'useResourceEffect';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
|
||||
@@ -41,6 +41,7 @@ let waitFor;
|
||||
let waitForThrow;
|
||||
let waitForPaint;
|
||||
let assertLog;
|
||||
let useResourceEffect;
|
||||
|
||||
describe('ReactHooksWithNoopRenderer', () => {
|
||||
beforeEach(() => {
|
||||
@@ -66,6 +67,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
useDeferredValue = React.useDeferredValue;
|
||||
Suspense = React.Suspense;
|
||||
Activity = React.unstable_Activity;
|
||||
useResourceEffect = React.experimental_useResourceEffect;
|
||||
ContinuousEventPriority =
|
||||
require('react-reconciler/constants').ContinuousEventPriority;
|
||||
if (gate(flags => flags.enableSuspenseList)) {
|
||||
@@ -3252,6 +3254,681 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
describe('useResourceEffect', () => {
|
||||
class Resource {
|
||||
isDeleted: false;
|
||||
id: string;
|
||||
opts: mixed;
|
||||
constructor(id, opts) {
|
||||
this.id = id;
|
||||
this.opts = opts;
|
||||
}
|
||||
update(opts) {
|
||||
if (this.isDeleted) {
|
||||
console.error('Cannot update deleted resource');
|
||||
return;
|
||||
}
|
||||
this.opts = opts;
|
||||
}
|
||||
destroy() {
|
||||
this.isDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('validates create return value', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(() => {
|
||||
Scheduler.log(`create(${id})`);
|
||||
}, [id]);
|
||||
return null;
|
||||
}
|
||||
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
'useResourceEffect must provide a callback which returns a resource. ' +
|
||||
'If a managed resource is not needed here, use useEffect. Received undefined',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('validates non-empty update deps', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(
|
||||
() => {
|
||||
Scheduler.log(`create(${id})`);
|
||||
return {};
|
||||
},
|
||||
[id],
|
||||
() => {
|
||||
Scheduler.log('update');
|
||||
},
|
||||
[],
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
'useResourceEffect received a dependency array with no dependencies. ' +
|
||||
'When specified, the dependency array must have at least one dependency.',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('simple mount and update', async () => {
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
resource.destroy();
|
||||
Scheduler.log(`destroy(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Jack" />);
|
||||
});
|
||||
assertLog(['create(1, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Lauren" />);
|
||||
});
|
||||
assertLog(['update(1, Lauren)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Lauren" />);
|
||||
});
|
||||
assertLog([]);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Jordan" />);
|
||||
});
|
||||
assertLog(['update(1, Jordan)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={2} username="Jack" />);
|
||||
});
|
||||
assertLog(['destroy(1, Jordan)', 'create(2, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(null);
|
||||
});
|
||||
assertLog(['destroy(2, Jack)']);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('simple mount with no update', async () => {
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
resource.destroy();
|
||||
Scheduler.log(`destroy(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Jack" />);
|
||||
});
|
||||
assertLog(['create(1, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(null);
|
||||
});
|
||||
assertLog(['destroy(1, Jack)']);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('calls update on every render if no deps are specified', async () => {
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Jack" />);
|
||||
});
|
||||
assertLog(['create(1, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={1} username="Jack" />);
|
||||
});
|
||||
assertLog(['update(1, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={2} username="Jack" />);
|
||||
});
|
||||
assertLog(['create(2, Jack)', 'update(2, Jack)']);
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(<App id={2} username="Lauren" />);
|
||||
});
|
||||
|
||||
assertLog(['update(2, Lauren)']);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('does not unmount previous useResourceEffect between updates', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id);
|
||||
Scheduler.log(`create(${resource.id})`);
|
||||
return resource;
|
||||
},
|
||||
[],
|
||||
resource => {
|
||||
Scheduler.log(`update(${resource.id})`);
|
||||
},
|
||||
undefined,
|
||||
resource => {
|
||||
Scheduler.log(`destroy(${resource.id})`);
|
||||
resource.destroy();
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
});
|
||||
|
||||
assertLog(['create(0)']);
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={1} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 1', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 1" />);
|
||||
});
|
||||
|
||||
assertLog(['update(0)']);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('unmounts only on deletion', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id);
|
||||
Scheduler.log(`create(${resource.id})`);
|
||||
return resource;
|
||||
},
|
||||
undefined,
|
||||
resource => {
|
||||
Scheduler.log(`update(${resource.id})`);
|
||||
},
|
||||
undefined,
|
||||
resource => {
|
||||
Scheduler.log(`destroy(${resource.id})`);
|
||||
resource.destroy();
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
});
|
||||
|
||||
assertLog(['create(0)']);
|
||||
|
||||
ReactNoop.render(null);
|
||||
await waitForAll(['destroy(0)']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('unmounts on deletion', async () => {
|
||||
function Wrapper(props) {
|
||||
return <App {...props} />;
|
||||
}
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
resource.destroy();
|
||||
Scheduler.log(`destroy(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<Wrapper id={0} username="Sathya" />, () =>
|
||||
Scheduler.log('Sync effect'),
|
||||
);
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
});
|
||||
|
||||
assertLog(['create(0, Sathya)']);
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<Wrapper id={0} username="Lauren" />, () =>
|
||||
Scheduler.log('Sync effect'),
|
||||
);
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
});
|
||||
|
||||
assertLog(['update(0, Lauren)']);
|
||||
|
||||
ReactNoop.render(null);
|
||||
await waitForAll(['destroy(0, Lauren)']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('handles errors in create on mount', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(
|
||||
() => {
|
||||
Scheduler.log(`Mount A [${id}]`);
|
||||
return {};
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
resource => {
|
||||
Scheduler.log(`Unmount A [${id}]`);
|
||||
},
|
||||
);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
Scheduler.log('Oops!');
|
||||
throw new Error('Oops!');
|
||||
// eslint-disable-next-line no-unreachable
|
||||
Scheduler.log(`Mount B [${id}]`);
|
||||
return {};
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
resource => {
|
||||
Scheduler.log(`Unmount B [${id}]`);
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
});
|
||||
}).rejects.toThrow('Oops');
|
||||
|
||||
assertLog([
|
||||
'Mount A [0]',
|
||||
'Oops!',
|
||||
// Clean up effect A. There's no effect B to clean-up, because it
|
||||
// never mounted.
|
||||
'Unmount A [0]',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('handles errors in create on update', async () => {
|
||||
function App({id}) {
|
||||
useResourceEffect(
|
||||
() => {
|
||||
Scheduler.log(`Mount A [${id}]`);
|
||||
return {};
|
||||
},
|
||||
[],
|
||||
() => {
|
||||
if (id === 1) {
|
||||
Scheduler.log('Oops!');
|
||||
throw new Error('Oops error!');
|
||||
}
|
||||
Scheduler.log(`Update A [${id}]`);
|
||||
},
|
||||
[id],
|
||||
() => {
|
||||
Scheduler.log(`Unmount A [${id}]`);
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog(['Mount A [0]']);
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
// This update will trigger an error
|
||||
ReactNoop.render(<App id={1} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor(['Id: 1', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 1" />);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog(['Oops!', 'Unmount A [1]']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
});
|
||||
}).rejects.toThrow('Oops error!');
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('handles errors in destroy on update', async () => {
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`Mount A [${id}, ${resource.opts.username}]`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`Update A [${id}, ${resource.opts.username}]`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
Scheduler.log(`Oops, ${resource.opts.username}!`);
|
||||
if (id === 1) {
|
||||
throw new Error(`Oops ${resource.opts.username} error!`);
|
||||
}
|
||||
Scheduler.log(`Unmount A [${id}, ${resource.opts.username}]`);
|
||||
},
|
||||
);
|
||||
return <Text text={'Id: ' + id} />;
|
||||
}
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} username="Lauren" />, () =>
|
||||
Scheduler.log('Sync effect'),
|
||||
);
|
||||
await waitFor(['Id: 0', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 0" />);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog(['Mount A [0, Lauren]']);
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
// This update will trigger an error during passive effect unmount
|
||||
ReactNoop.render(<App id={1} username="Sathya" />, () =>
|
||||
Scheduler.log('Sync effect'),
|
||||
);
|
||||
await waitFor(['Id: 1', 'Sync effect']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop="Id: 1" />);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog(['Oops, Lauren!', 'Mount A [1, Sathya]', 'Oops, Sathya!']);
|
||||
});
|
||||
// TODO(lauren) more explicit assertions. this is weird because we
|
||||
// destroy both the first and second resource
|
||||
}).rejects.toThrow();
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook && enableActivity
|
||||
it('composes with activity', async () => {
|
||||
function App({id, username}) {
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
resource.destroy();
|
||||
Scheduler.log(`destroy(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<App id={0} username="Rick" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<App id={0} username="Lauren" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<App id={0} username="Rick" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['create(0, Rick)']);
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<App id={0} username="Lauren" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['update(0, Lauren)']);
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<App id={0} username="Lauren" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['destroy(0, Lauren)']);
|
||||
});
|
||||
|
||||
// @gate enableUseResourceEffectHook
|
||||
it('composes with suspense', async () => {
|
||||
function TextBox({text}) {
|
||||
return <AsyncText text={text} ms={0} />;
|
||||
}
|
||||
let setUsername_;
|
||||
function App({id}) {
|
||||
const [username, setUsername] = useState('Mofei');
|
||||
setUsername_ = setUsername;
|
||||
const opts = useMemo(() => {
|
||||
return {username};
|
||||
}, [username]);
|
||||
useResourceEffect(
|
||||
() => {
|
||||
const resource = new Resource(id, opts);
|
||||
Scheduler.log(`create(${resource.id}, ${resource.opts.username})`);
|
||||
return resource;
|
||||
},
|
||||
[id],
|
||||
resource => {
|
||||
resource.update(opts);
|
||||
Scheduler.log(`update(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
[opts],
|
||||
resource => {
|
||||
resource.destroy();
|
||||
Scheduler.log(`destroy(${resource.id}, ${resource.opts.username})`);
|
||||
},
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Text text={'Sync: ' + username} />
|
||||
<Suspense fallback={<Text text={'Loading'} />}>
|
||||
<TextBox text={username} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={0} />);
|
||||
await waitFor([
|
||||
'Sync: Mofei',
|
||||
'Suspend! [Mofei]',
|
||||
'Loading',
|
||||
'create(0, Mofei)',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Sync: Mofei" />
|
||||
<span prop="Loading" />
|
||||
</>,
|
||||
);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog([]);
|
||||
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
await advanceTimers(10);
|
||||
assertLog(['Promise resolved [Mofei]']);
|
||||
});
|
||||
assertLog(['Mofei']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Sync: Mofei" />
|
||||
<span prop="Mofei" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(<App id={1} />, () => Scheduler.log('Sync effect'));
|
||||
await waitFor([
|
||||
'Sync: Mofei',
|
||||
'Mofei',
|
||||
'Sync effect',
|
||||
'destroy(0, Mofei)',
|
||||
'create(1, Mofei)',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Sync: Mofei" />
|
||||
<span prop="Mofei" />
|
||||
</>,
|
||||
);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog([]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
setUsername_('Lauren');
|
||||
await waitFor([
|
||||
'Sync: Lauren',
|
||||
'Suspend! [Lauren]',
|
||||
'Loading',
|
||||
'update(1, Lauren)',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Sync: Lauren" />
|
||||
<span hidden={true} prop="Mofei" />
|
||||
<span prop="Loading" />
|
||||
</>,
|
||||
);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
assertLog([]);
|
||||
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
await advanceTimers(10);
|
||||
assertLog(['Promise resolved [Lauren]']);
|
||||
});
|
||||
assertLog(['Lauren']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Sync: Lauren" />
|
||||
<span prop="Lauren" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCallback', () => {
|
||||
it('memoizes callback by comparing inputs', async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
|
||||
@@ -60,6 +60,7 @@ export {
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
experimental_useEffectEvent,
|
||||
experimental_useResourceEffect,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -41,6 +41,7 @@ export {
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
experimental_useEffectEvent,
|
||||
experimental_useResourceEffect,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -19,6 +19,7 @@ export {
|
||||
createElement,
|
||||
createRef,
|
||||
experimental_useEffectEvent,
|
||||
experimental_useResourceEffect,
|
||||
forwardRef,
|
||||
Fragment,
|
||||
isValidElement,
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useResourceEffect,
|
||||
useImperativeHandle,
|
||||
useDebugValue,
|
||||
useInsertionEffect,
|
||||
@@ -89,6 +90,7 @@ export {
|
||||
useContext,
|
||||
useEffect,
|
||||
useEffectEvent as experimental_useEffectEvent,
|
||||
useResourceEffect as experimental_useResourceEffect,
|
||||
useImperativeHandle,
|
||||
useDebugValue,
|
||||
useInsertionEffect,
|
||||
|
||||
@@ -18,7 +18,10 @@ import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
|
||||
import {enableAsyncActions} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableAsyncActions,
|
||||
enableUseResourceEffectHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableContextProfiling,
|
||||
enableLazyContextPropagation,
|
||||
@@ -233,7 +236,18 @@ export function useResourceEffect(
|
||||
updateDeps: Array<mixed> | void | null,
|
||||
destroy: ((resource: mixed) => void) | void,
|
||||
): void {
|
||||
throw new Error('Not implemented.');
|
||||
if (!enableUseResourceEffectHook) {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
const dispatcher = resolveDispatcher();
|
||||
// $FlowFixMe[not-a-function] This is unstable, thus optional
|
||||
return dispatcher.useResourceEffect(
|
||||
create,
|
||||
createDeps,
|
||||
update,
|
||||
updateDeps,
|
||||
destroy,
|
||||
);
|
||||
}
|
||||
|
||||
export function useOptimistic<S, A>(
|
||||
|
||||
Reference in New Issue
Block a user