Compare commits

...

1 Commits

Author SHA1 Message Date
Rick Hanlon
6e33440a44 wip fire 2024-11-04 16:54:52 -05:00
13 changed files with 1013 additions and 0 deletions

View File

@@ -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;

View 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']);
});
});

View File

@@ -8,3 +8,4 @@
*/
export {useMemoCache as c} from './src/ReactHooks';
export {useFire as f} from './src/ReactHooks';

View File

@@ -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 = () => {};

View File

@@ -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,

View File

@@ -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

View File

@@ -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);

View File

@@ -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__;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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.

View File

@@ -38,6 +38,7 @@ export const {
retryLaneExpirationMs,
syncLaneExpirationMs,
transitionLaneExpirationMs,
enableFire,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.