Compare commits
1 Commits
asserts-st
...
rh/fire
| 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,
|
disableLegacyMode,
|
||||||
enableNoCloningMemoCache,
|
enableNoCloningMemoCache,
|
||||||
enableContextProfiling,
|
enableContextProfiling,
|
||||||
|
enableFire,
|
||||||
} from 'shared/ReactFeatureFlags';
|
} from 'shared/ReactFeatureFlags';
|
||||||
import {
|
import {
|
||||||
REACT_CONTEXT_TYPE,
|
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(
|
function mountInsertionEffect(
|
||||||
create: () => (() => void) | void,
|
create: () => (() => void) | void,
|
||||||
deps: Array<mixed> | void | null,
|
deps: Array<mixed> | void | null,
|
||||||
@@ -3789,6 +3844,9 @@ if (enableUseMemoCacheHook) {
|
|||||||
if (enableUseEffectEventHook) {
|
if (enableUseEffectEventHook) {
|
||||||
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
|
(ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
|
||||||
}
|
}
|
||||||
|
if (enableFire) {
|
||||||
|
(ContextOnlyDispatcher: Dispatcher).useFire = throwInvalidHookError;
|
||||||
|
}
|
||||||
if (enableAsyncActions) {
|
if (enableAsyncActions) {
|
||||||
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
|
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
|
||||||
throwInvalidHookError;
|
throwInvalidHookError;
|
||||||
@@ -3832,6 +3890,9 @@ if (enableUseMemoCacheHook) {
|
|||||||
if (enableUseEffectEventHook) {
|
if (enableUseEffectEventHook) {
|
||||||
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
|
(HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
|
||||||
}
|
}
|
||||||
|
if (enableFire) {
|
||||||
|
(HooksDispatcherOnMount: Dispatcher).useFire = mountFire;
|
||||||
|
}
|
||||||
if (enableAsyncActions) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -3875,6 +3936,9 @@ if (enableUseMemoCacheHook) {
|
|||||||
if (enableUseEffectEventHook) {
|
if (enableUseEffectEventHook) {
|
||||||
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
|
(HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
|
||||||
}
|
}
|
||||||
|
if (enableFire) {
|
||||||
|
(HooksDispatcherOnUpdate: Dispatcher).useFire = updateFire;
|
||||||
|
}
|
||||||
if (enableAsyncActions) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -3918,6 +3982,9 @@ if (enableUseMemoCacheHook) {
|
|||||||
if (enableUseEffectEventHook) {
|
if (enableUseEffectEventHook) {
|
||||||
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
|
(HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
|
||||||
}
|
}
|
||||||
|
if (enableFire) {
|
||||||
|
(HooksDispatcherOnRerender: Dispatcher).useFire = updateFire;
|
||||||
|
}
|
||||||
if (enableAsyncActions) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -4108,6 +4175,17 @@ if (__DEV__) {
|
|||||||
return mountEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -4300,6 +4378,16 @@ if (__DEV__) {
|
|||||||
return mountEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -4491,6 +4579,17 @@ if (__DEV__) {
|
|||||||
return updateEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -4682,6 +4781,17 @@ if (__DEV__) {
|
|||||||
return updateEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -4897,6 +5007,17 @@ if (__DEV__) {
|
|||||||
return mountEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -5115,6 +5236,17 @@ if (__DEV__) {
|
|||||||
return updateEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
useHostTransitionStatus;
|
useHostTransitionStatus;
|
||||||
@@ -5333,6 +5465,17 @@ if (__DEV__) {
|
|||||||
return updateEvent(callback);
|
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) {
|
if (enableAsyncActions) {
|
||||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||||
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 {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
|
// export to match the name of the OSS function typically exported from
|
||||||
// react/compiler-runtime
|
// react/compiler-runtime
|
||||||
export {useMemoCache as c} from './src/ReactHooks';
|
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);
|
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>(
|
export function useOptimistic<S, A>(
|
||||||
passthrough: S,
|
passthrough: S,
|
||||||
reducer: ?(S, A) => S,
|
reducer: ?(S, A) => S,
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export const enableUseMemoCacheHook = true;
|
|||||||
export const enableNoCloningMemoCache = false;
|
export const enableNoCloningMemoCache = false;
|
||||||
|
|
||||||
export const enableUseEffectEventHook = __EXPERIMENTAL__;
|
export const enableUseEffectEventHook = __EXPERIMENTAL__;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Test in www before enabling in open source.
|
// Test in www before enabling in open source.
|
||||||
// Enables DOM-server to stream its instruction set as data-attributes
|
// 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 syncLaneExpirationMs = 250;
|
||||||
export const transitionLaneExpirationMs = 5000;
|
export const transitionLaneExpirationMs = 5000;
|
||||||
export const useModernStrictMode = true;
|
export const useModernStrictMode = true;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Flow magic to verify the exports of this file match the original version.
|
// Flow magic to verify the exports of this file match the original version.
|
||||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const syncLaneExpirationMs = 250;
|
|||||||
export const transitionLaneExpirationMs = 5000;
|
export const transitionLaneExpirationMs = 5000;
|
||||||
export const useModernStrictMode = true;
|
export const useModernStrictMode = true;
|
||||||
export const enableSiblingPrerendering = false;
|
export const enableSiblingPrerendering = false;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Profiling Only
|
// Profiling Only
|
||||||
export const enableProfilerTimer = __PROFILE__;
|
export const enableProfilerTimer = __PROFILE__;
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export const disableDefaultPropsExceptForClasses = true;
|
|||||||
|
|
||||||
export const enableObjectFiber = false;
|
export const enableObjectFiber = false;
|
||||||
export const enableOwnerStacks = false;
|
export const enableOwnerStacks = false;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Flow magic to verify the exports of this file match the original version.
|
// Flow magic to verify the exports of this file match the original version.
|
||||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export const transitionLaneExpirationMs = 5000;
|
|||||||
export const useModernStrictMode = true;
|
export const useModernStrictMode = true;
|
||||||
export const enableFabricCompleteRootInCommitPhase = false;
|
export const enableFabricCompleteRootInCommitPhase = false;
|
||||||
export const enableSiblingPrerendering = false;
|
export const enableSiblingPrerendering = false;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Flow magic to verify the exports of this file match the original version.
|
// Flow magic to verify the exports of this file match the original version.
|
||||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const enableObjectFiber = false;
|
|||||||
export const enableOwnerStacks = false;
|
export const enableOwnerStacks = false;
|
||||||
export const enableShallowPropDiffing = false;
|
export const enableShallowPropDiffing = false;
|
||||||
export const enableSiblingPrerendering = false;
|
export const enableSiblingPrerendering = false;
|
||||||
|
export const enableFire = false;
|
||||||
|
|
||||||
// Flow magic to verify the exports of this file match the original version.
|
// Flow magic to verify the exports of this file match the original version.
|
||||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const renameElementSymbol = __VARIANT__;
|
|||||||
export const retryLaneExpirationMs = 5000;
|
export const retryLaneExpirationMs = 5000;
|
||||||
export const syncLaneExpirationMs = 250;
|
export const syncLaneExpirationMs = 250;
|
||||||
export const transitionLaneExpirationMs = 5000;
|
export const transitionLaneExpirationMs = 5000;
|
||||||
|
export const enableFire = __VARIANT__;
|
||||||
|
|
||||||
// Enable this flag to help with concurrent mode debugging.
|
// Enable this flag to help with concurrent mode debugging.
|
||||||
// It logs information to the console about React scheduling, rendering, and commit phases.
|
// It logs information to the console about React scheduling, rendering, and commit phases.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const {
|
|||||||
retryLaneExpirationMs,
|
retryLaneExpirationMs,
|
||||||
syncLaneExpirationMs,
|
syncLaneExpirationMs,
|
||||||
transitionLaneExpirationMs,
|
transitionLaneExpirationMs,
|
||||||
|
enableFire,
|
||||||
} = dynamicFeatureFlags;
|
} = dynamicFeatureFlags;
|
||||||
|
|
||||||
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
||||||
|
|||||||
Reference in New Issue
Block a user