Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e33440a44 |
143
packages/react-reconciler/src/ReactFiberHooks.js
vendored
143
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -48,6 +48,7 @@ import {
|
||||
disableLegacyMode,
|
||||
enableNoCloningMemoCache,
|
||||
enableContextProfiling,
|
||||
enableFire,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
REACT_CONTEXT_TYPE,
|
||||
@@ -2693,6 +2694,60 @@ function updateEvent<Args, Return, F: (...Array<Args>) => Return>(
|
||||
};
|
||||
}
|
||||
|
||||
function useFireImpl<Args, Return, F: (...Array<Args>) => Return>(
|
||||
payload: EventFunctionPayload<Args, Return, F>,
|
||||
) {
|
||||
currentlyRenderingFiber.flags |= UpdateEffect;
|
||||
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
|
||||
(currentlyRenderingFiber.updateQueue: any);
|
||||
if (componentUpdateQueue === null) {
|
||||
componentUpdateQueue = createFunctionComponentUpdateQueue();
|
||||
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
|
||||
componentUpdateQueue.events = [payload];
|
||||
} else {
|
||||
const events = componentUpdateQueue.events;
|
||||
if (events === null) {
|
||||
componentUpdateQueue.events = [payload];
|
||||
} else {
|
||||
events.push(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mountFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const ref = {impl: callback};
|
||||
hook.memoizedState = ref;
|
||||
// $FlowIgnore[incompatible-return]
|
||||
return function eventFn() {
|
||||
if (isInvalidExecutionContextForEventFunction()) {
|
||||
throw new Error(
|
||||
"A function wrapped in useFire can't be called during rendering.",
|
||||
);
|
||||
}
|
||||
return ref.impl.apply(undefined, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
function updateFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const ref = hook.memoizedState;
|
||||
useFireImpl({ref, nextImpl: callback});
|
||||
// $FlowIgnore[incompatible-return]
|
||||
return function eventFn() {
|
||||
if (isInvalidExecutionContextForEventFunction()) {
|
||||
throw new Error(
|
||||
"A function wrapped in useFire can't be called during rendering.",
|
||||
);
|
||||
}
|
||||
return ref.impl.apply(undefined, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
function mountInsertionEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
@@ -3789,6 +3844,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
|
||||
}
|
||||
if (enableFire) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useFire = throwInvalidHookError;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
|
||||
throwInvalidHookError;
|
||||
@@ -3832,6 +3890,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useFire = mountFire;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -3875,6 +3936,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useFire = updateFire;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -3918,6 +3982,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseEffectEventHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useFire = updateFire;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4108,6 +4175,17 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useFire = function useFire<
|
||||
Args,
|
||||
Return,
|
||||
F: (...Array<Args>) => Return,
|
||||
>(callback: F): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
mountHookTypesDev();
|
||||
return mountFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4300,6 +4378,16 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFire =
|
||||
function useFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
updateHookTypesDev();
|
||||
return mountFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4491,6 +4579,17 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFire = function useFire<
|
||||
Args,
|
||||
Return,
|
||||
F: (...Array<Args>) => Return,
|
||||
>(callback: F): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
updateHookTypesDev();
|
||||
return updateFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4682,6 +4781,17 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFire = function useFire<
|
||||
Args,
|
||||
Return,
|
||||
F: (...Array<Args>) => Return,
|
||||
>(callback: F): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
updateHookTypesDev();
|
||||
return updateFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -4897,6 +5007,17 @@ if (__DEV__) {
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFire =
|
||||
function useFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -5115,6 +5236,17 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFire =
|
||||
function useFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
@@ -5333,6 +5465,17 @@ if (__DEV__) {
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
if (enableFire) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFire =
|
||||
function useFire<Args, Return, F: (...Array<Args>) => Return>(
|
||||
callback: F,
|
||||
): F {
|
||||
currentHookNameInDev = 'useFire';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateFire(callback);
|
||||
};
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
|
||||
853
packages/react-reconciler/src/__tests__/useFire-test.js
vendored
Normal file
853
packages/react-reconciler/src/__tests__/useFire-test.js
vendored
Normal file
@@ -0,0 +1,853 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {useInsertionEffect} from 'react';
|
||||
|
||||
describe('useFire', () => {
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let createContext;
|
||||
let useContext;
|
||||
let useState;
|
||||
let useFire;
|
||||
let useEffect;
|
||||
let useLayoutEffect;
|
||||
let useMemo;
|
||||
let waitForAll;
|
||||
let assertLog;
|
||||
let waitForThrow;
|
||||
|
||||
beforeEach(() => {
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
act = require('internal-test-utils').act;
|
||||
createContext = React.createContext;
|
||||
useContext = React.useContext;
|
||||
useState = React.useState;
|
||||
useFire = require('react/compiler-runtime').f;
|
||||
useEffect = React.useEffect;
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
useMemo = React.useMemo;
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
waitForAll = InternalTestUtils.waitForAll;
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
waitForThrow = InternalTestUtils.waitForThrow;
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
Scheduler.log(props.text);
|
||||
return <span prop={props.text} />;
|
||||
}
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('memoizes basic case correctly', async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const onClick = useFire(() => updateCount(c => c + incrementBy));
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => onClick()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
await waitForAll(['Increment', 'Count: 0']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 0" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 1']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 1" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
await waitForAll(['Increment', 'Count: 2']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Event uses the new prop
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 12']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 12" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('can be defined more than once', async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
multiply = () => {
|
||||
this.props.onMouseEnter();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const onClick = useFire(() => updateCount(c => c + incrementBy));
|
||||
const onMouseEnter = useFire(() => {
|
||||
updateCount(c => c * incrementBy);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton
|
||||
onClick={() => onClick()}
|
||||
onMouseEnter={() => onMouseEnter()}
|
||||
ref={button}
|
||||
/>
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={5} />);
|
||||
await waitForAll(['Increment', 'Count: 0']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 0" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 5']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 5" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.multiply());
|
||||
assertLog(['Increment', 'Count: 25']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 25" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('does not preserve `this` in event functions', async () => {
|
||||
class GreetButton extends React.PureComponent {
|
||||
greet = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text={'Say ' + this.props.hello} />;
|
||||
}
|
||||
}
|
||||
function Greeter({hello}) {
|
||||
const person = {
|
||||
toString() {
|
||||
return 'Jane';
|
||||
},
|
||||
greet() {
|
||||
return updateGreeting(this + ' says ' + hello);
|
||||
},
|
||||
};
|
||||
const [greeting, updateGreeting] = useState('Seb says ' + hello);
|
||||
const onClick = useFire(person.greet);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GreetButton hello={hello} onClick={() => onClick()} ref={button} />
|
||||
<Text text={'Greeting: ' + greeting} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Greeter hello={'hej'} />);
|
||||
await waitForAll(['Say hej', 'Greeting: Seb says hej']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Say hej" />
|
||||
<span prop="Greeting: Seb says hej" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.greet());
|
||||
assertLog(['Say hej', 'Greeting: undefined says hej']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Say hej" />
|
||||
<span prop="Greeting: undefined says hej" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('throws when called in render', async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
|
||||
render() {
|
||||
// Will throw.
|
||||
this.props.onClick();
|
||||
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const onClick = useFire(() => updateCount(c => c + incrementBy));
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => onClick()} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
await waitForThrow(
|
||||
"A function wrapped in useFire can't be called during rendering.",
|
||||
);
|
||||
assertLog([]);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it("useLayoutEffect shouldn't re-fire when event handlers change", async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useFire(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
assertLog([]);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 3" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 4" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 24" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Event uses the new prop
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 34" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it("useEffect shouldn't re-fire when event handlers change", async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useFire(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Scheduler.log('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 3" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 4" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 24" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Event uses the new prop
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 34" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('is stable in a custom hook', async () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function useCount(incrementBy) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useFire(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
return [count, increment];
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, increment] = useCount(incrementBy);
|
||||
|
||||
useEffect(() => {
|
||||
Scheduler.log('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 2" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 3" />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => button.current.increment());
|
||||
assertLog([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 4" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
await waitForAll([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 24" />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Event uses the new prop
|
||||
await act(() => button.current.increment());
|
||||
assertLog(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Increment" />
|
||||
<span prop="Count: 34" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('is mutated before all other effects', async () => {
|
||||
function Counter({value}) {
|
||||
useInsertionEffect(() => {
|
||||
Scheduler.log('Effect value: ' + value);
|
||||
increment();
|
||||
}, [value]);
|
||||
|
||||
// This is defined after the insertion effect, but it should
|
||||
// update the event fn _before_ the insertion effect fires.
|
||||
const increment = useFire(() => {
|
||||
Scheduler.log('Event value: ' + value);
|
||||
});
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Counter value={1} />);
|
||||
await waitForAll(['Effect value: 1', 'Event value: 1']);
|
||||
|
||||
await act(() => ReactNoop.render(<Counter value={2} />));
|
||||
assertLog(['Effect value: 2', 'Event value: 2']);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it("doesn't provide a stable identity", async () => {
|
||||
function Counter({shouldRender, value}) {
|
||||
const onClick = useFire(() => {
|
||||
Scheduler.log(
|
||||
'onClick, shouldRender=' + shouldRender + ', value=' + value,
|
||||
);
|
||||
});
|
||||
|
||||
// onClick doesn't have a stable function identity so this effect will fire on every render.
|
||||
// In a real app useFire functions should *not* be passed as a dependency, this is for
|
||||
// testing purposes only.
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
}, [onClick]);
|
||||
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
}, [shouldRender]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Counter shouldRender={true} value={0} />);
|
||||
await waitForAll([
|
||||
'onClick, shouldRender=true, value=0',
|
||||
'onClick, shouldRender=true, value=0',
|
||||
]);
|
||||
|
||||
ReactNoop.render(<Counter shouldRender={true} value={1} />);
|
||||
await waitForAll(['onClick, shouldRender=true, value=1']);
|
||||
|
||||
ReactNoop.render(<Counter shouldRender={false} value={2} />);
|
||||
await waitForAll([
|
||||
'onClick, shouldRender=false, value=2',
|
||||
'onClick, shouldRender=false, value=2',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('event handlers always see the latest committed value', async () => {
|
||||
let committedEventHandler = null;
|
||||
|
||||
function App({value}) {
|
||||
const event = useFire(() => {
|
||||
return 'Value seen by useFire: ' + value;
|
||||
});
|
||||
|
||||
// Set up an effect that registers the event handler with an external
|
||||
// event system (e.g. addEventListener).
|
||||
useEffect(
|
||||
() => {
|
||||
// Log when the effect fires. In the test below, we'll assert that this
|
||||
// only happens during initial render, not during updates.
|
||||
Scheduler.log('Commit new event handler');
|
||||
committedEventHandler = event;
|
||||
return () => {
|
||||
committedEventHandler = null;
|
||||
};
|
||||
},
|
||||
// Note that we've intentionally omitted the event from the dependency
|
||||
// array. But it will still be able to see the latest `value`. This is the
|
||||
// key feature of useFire that makes it different from a regular closure.
|
||||
[],
|
||||
);
|
||||
return 'Latest rendered value ' + value;
|
||||
}
|
||||
|
||||
// Initial render
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => {
|
||||
root.render(<App value={1} />);
|
||||
});
|
||||
assertLog(['Commit new event handler']);
|
||||
expect(root).toMatchRenderedOutput('Latest rendered value 1');
|
||||
expect(committedEventHandler()).toBe('Value seen by useFire: 1');
|
||||
|
||||
// Update
|
||||
await act(() => {
|
||||
root.render(<App value={2} />);
|
||||
});
|
||||
// No new event handler should be committed, because it was omitted from
|
||||
// the dependency array.
|
||||
assertLog([]);
|
||||
// But the event handler should still be able to see the latest value.
|
||||
expect(root).toMatchRenderedOutput('Latest rendered value 2');
|
||||
expect(committedEventHandler()).toBe('Value seen by useFire: 2');
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('integration: implements docs chat room example', async () => {
|
||||
function createConnection() {
|
||||
let connectedCallback;
|
||||
let timeout;
|
||||
return {
|
||||
connect() {
|
||||
timeout = setTimeout(() => {
|
||||
if (connectedCallback) {
|
||||
connectedCallback();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
on(event, callback) {
|
||||
if (connectedCallback) {
|
||||
throw Error('Cannot add the handler twice.');
|
||||
}
|
||||
if (event !== 'connected') {
|
||||
throw Error('Only "connected" event is supported.');
|
||||
}
|
||||
connectedCallback = callback;
|
||||
},
|
||||
disconnect() {
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ChatRoom({roomId, theme}) {
|
||||
const onConnected = useFire(() => {
|
||||
Scheduler.log('Connected! theme: ' + theme);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId);
|
||||
connection.on('connected', () => {
|
||||
onConnected();
|
||||
});
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [roomId]);
|
||||
|
||||
return <Text text={`Welcome to the ${roomId} room!`} />;
|
||||
}
|
||||
|
||||
await act(() =>
|
||||
ReactNoop.render(<ChatRoom roomId="general" theme="light" />),
|
||||
);
|
||||
|
||||
assertLog(['Welcome to the general room!', 'Connected! theme: light']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<span prop="Welcome to the general room!" />,
|
||||
);
|
||||
|
||||
// change roomId only
|
||||
await act(() =>
|
||||
ReactNoop.render(<ChatRoom roomId="music" theme="light" />),
|
||||
);
|
||||
assertLog([
|
||||
'Welcome to the music room!',
|
||||
// should trigger a reconnect
|
||||
'Connected! theme: light',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<span prop="Welcome to the music room!" />,
|
||||
);
|
||||
|
||||
// change theme only
|
||||
await act(() => ReactNoop.render(<ChatRoom roomId="music" theme="dark" />));
|
||||
// should not trigger a reconnect
|
||||
assertLog(['Welcome to the music room!']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<span prop="Welcome to the music room!" />,
|
||||
);
|
||||
|
||||
// change roomId only
|
||||
await act(() =>
|
||||
ReactNoop.render(<ChatRoom roomId="travel" theme="dark" />),
|
||||
);
|
||||
assertLog([
|
||||
'Welcome to the travel room!',
|
||||
// should trigger a reconnect
|
||||
'Connected! theme: dark',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<span prop="Welcome to the travel room!" />,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableUseEffectEventHook
|
||||
it('integration: implements the docs logVisit example', async () => {
|
||||
class AddToCartButton extends React.PureComponent {
|
||||
addToCart = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Add to cart" />;
|
||||
}
|
||||
}
|
||||
const ShoppingCartContext = createContext(null);
|
||||
|
||||
function AppShell({children}) {
|
||||
const [items, updateItems] = useState([]);
|
||||
const value = useMemo(() => ({items, updateItems}), [items, updateItems]);
|
||||
|
||||
return (
|
||||
<ShoppingCartContext.Provider value={value}>
|
||||
{children}
|
||||
</ShoppingCartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Page({url}) {
|
||||
const {items, updateItems} = useContext(ShoppingCartContext);
|
||||
const onClick = useFire(() => updateItems([...items, 1]));
|
||||
const numberOfItems = items.length;
|
||||
|
||||
const onVisit = useFire(visitedUrl => {
|
||||
Scheduler.log(
|
||||
'url: ' + visitedUrl + ', numberOfItems: ' + numberOfItems,
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onVisit(url);
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<AddToCartButton
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
ref={button}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
await act(() =>
|
||||
ReactNoop.render(
|
||||
<AppShell>
|
||||
<Page url="/shop/1" />
|
||||
</AppShell>,
|
||||
),
|
||||
);
|
||||
assertLog(['Add to cart', 'url: /shop/1, numberOfItems: 0']);
|
||||
await act(() => button.current.addToCart());
|
||||
assertLog(['Add to cart']);
|
||||
|
||||
await act(() =>
|
||||
ReactNoop.render(
|
||||
<AppShell>
|
||||
<Page url="/shop/2" />
|
||||
</AppShell>,
|
||||
),
|
||||
);
|
||||
assertLog(['Add to cart', 'url: /shop/2, numberOfItems: 1']);
|
||||
});
|
||||
});
|
||||
@@ -8,3 +8,4 @@
|
||||
*/
|
||||
|
||||
export {useMemoCache as c} from './src/ReactHooks';
|
||||
export {useFire as f} from './src/ReactHooks';
|
||||
|
||||
@@ -67,3 +67,5 @@ export {useMemoCache as unstable_useMemoCache} from './src/ReactHooks';
|
||||
// export to match the name of the OSS function typically exported from
|
||||
// react/compiler-runtime
|
||||
export {useMemoCache as c} from './src/ReactHooks';
|
||||
|
||||
export const fire: () => void = () => {};
|
||||
|
||||
@@ -226,6 +226,12 @@ export function useEffectEvent<Args, F: (...Array<Args>) => mixed>(
|
||||
return dispatcher.useEffectEvent(callback);
|
||||
}
|
||||
|
||||
export function useFire<Args, F: (...Array<Args>) => mixed>(callback: F): F {
|
||||
const dispatcher = resolveDispatcher();
|
||||
// $FlowFixMe[not-a-function] This is unstable, thus optional
|
||||
return dispatcher.useFire(callback);
|
||||
}
|
||||
|
||||
export function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
|
||||
@@ -123,6 +123,7 @@ export const enableUseMemoCacheHook = true;
|
||||
export const enableNoCloningMemoCache = false;
|
||||
|
||||
export const enableUseEffectEventHook = __EXPERIMENTAL__;
|
||||
export const enableFire = false;
|
||||
|
||||
// Test in www before enabling in open source.
|
||||
// Enables DOM-server to stream its instruction set as data-attributes
|
||||
|
||||
@@ -94,6 +94,7 @@ export const retryLaneExpirationMs = 5000;
|
||||
export const syncLaneExpirationMs = 250;
|
||||
export const transitionLaneExpirationMs = 5000;
|
||||
export const useModernStrictMode = true;
|
||||
export const enableFire = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -86,6 +86,7 @@ export const syncLaneExpirationMs = 250;
|
||||
export const transitionLaneExpirationMs = 5000;
|
||||
export const useModernStrictMode = true;
|
||||
export const enableSiblingPrerendering = false;
|
||||
export const enableFire = false;
|
||||
|
||||
// Profiling Only
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
|
||||
@@ -101,6 +101,7 @@ export const disableDefaultPropsExceptForClasses = true;
|
||||
|
||||
export const enableObjectFiber = false;
|
||||
export const enableOwnerStacks = false;
|
||||
export const enableFire = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -81,6 +81,7 @@ export const transitionLaneExpirationMs = 5000;
|
||||
export const useModernStrictMode = true;
|
||||
export const enableFabricCompleteRootInCommitPhase = false;
|
||||
export const enableSiblingPrerendering = false;
|
||||
export const enableFire = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -96,6 +96,7 @@ export const enableObjectFiber = false;
|
||||
export const enableOwnerStacks = false;
|
||||
export const enableShallowPropDiffing = false;
|
||||
export const enableSiblingPrerendering = false;
|
||||
export const enableFire = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -33,6 +33,7 @@ export const renameElementSymbol = __VARIANT__;
|
||||
export const retryLaneExpirationMs = 5000;
|
||||
export const syncLaneExpirationMs = 250;
|
||||
export const transitionLaneExpirationMs = 5000;
|
||||
export const enableFire = __VARIANT__;
|
||||
|
||||
// Enable this flag to help with concurrent mode debugging.
|
||||
// It logs information to the console about React scheduling, rendering, and commit phases.
|
||||
|
||||
@@ -38,6 +38,7 @@ export const {
|
||||
retryLaneExpirationMs,
|
||||
syncLaneExpirationMs,
|
||||
transitionLaneExpirationMs,
|
||||
enableFire,
|
||||
} = dynamicFeatureFlags;
|
||||
|
||||
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
||||
|
||||
Reference in New Issue
Block a user