Skip to content

Commit 1b7d2b7

Browse files
committed
Refactor useEvent
Previously, the useEvent implementation made use of effect infra under the hood. This was a lot of extra overhead for functionality we didn't use (events have no deps, and no clean up functions). This PR refactors the implementation to instead use a queue to ensure that the callback is stable across renders.
1 parent 3de9264 commit 1b7d2b7

6 files changed

Lines changed: 161 additions & 68 deletions

File tree

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ import {
164164
Layout as HookLayout,
165165
Insertion as HookInsertion,
166166
Passive as HookPassive,
167-
Snapshot as HookSnapshot,
168167
} from './ReactHookEffectTags';
169168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
170169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
416415
case FunctionComponent: {
417416
if (enableUseEventHook) {
418417
if ((flags & Update) !== NoFlags) {
419-
// useEvent doesn't need to be cleaned up
420-
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418+
commitUseEventMount(finishedWork);
421419
}
422420
}
423421
break;
@@ -665,6 +663,17 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
665663
}
666664
}
667665

666+
function commitUseEventMount(finishedWork: Fiber) {
667+
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
668+
const eventStates = updateQueue !== null ? updateQueue.events : null;
669+
if (eventStates !== null) {
670+
for (let ii = 0; ii < eventStates.length; ii++) {
671+
const eventState = eventStates[ii];
672+
eventState.event._current = eventState.nextEvent;
673+
}
674+
}
675+
}
676+
668677
export function commitPassiveEffectDurations(
669678
finishedRoot: FiberRoot,
670679
finishedWork: Fiber,

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ import {
164164
Layout as HookLayout,
165165
Insertion as HookInsertion,
166166
Passive as HookPassive,
167-
Snapshot as HookSnapshot,
168167
} from './ReactHookEffectTags';
169168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
170169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -416,8 +415,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
416415
case FunctionComponent: {
417416
if (enableUseEventHook) {
418417
if ((flags & Update) !== NoFlags) {
419-
// useEvent doesn't need to be cleaned up
420-
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
418+
commitUseEventMount(finishedWork);
421419
}
422420
}
423421
break;
@@ -665,6 +663,17 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
665663
}
666664
}
667665

666+
function commitUseEventMount(finishedWork: Fiber) {
667+
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
668+
const eventStates = updateQueue !== null ? updateQueue.events : null;
669+
if (eventStates !== null) {
670+
for (let ii = 0; ii < eventStates.length; ii++) {
671+
const eventState = eventStates[ii];
672+
eventState.event._current = eventState.nextEvent;
673+
}
674+
}
675+
}
676+
668677
export function commitPassiveEffectDurations(
669678
finishedRoot: FiberRoot,
670679
finishedWork: Fiber,

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ import {
8686
Layout as HookLayout,
8787
Passive as HookPassive,
8888
Insertion as HookInsertion,
89-
Snapshot as HookSnapshot,
9089
} from './ReactHookEffectTags';
9190
import {
9291
getWorkInProgressRoot,
@@ -182,8 +181,19 @@ type StoreConsistencyCheck<T> = {
182181
getSnapshot: () => T,
183182
};
184183

184+
type EventFunctionState<T> = {
185+
event: EventFunctionWrapper<T>,
186+
nextEvent: EventFunction<T>,
187+
};
188+
type EventFunctionWrapper<T> = {
189+
(): T,
190+
_current: EventFunction<T>,
191+
};
192+
type EventFunction<T> = () => T;
193+
185194
export type FunctionComponentUpdateQueue = {
186195
lastEffect: Effect | null,
196+
events: Array<EventFunctionState<any>> | null,
187197
stores: Array<StoreConsistencyCheck<any>> | null,
188198
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
189199
memoCache?: MemoCache | null,
@@ -727,6 +737,7 @@ if (enableUseMemoCacheHook) {
727737
createFunctionComponentUpdateQueue = () => {
728738
return {
729739
lastEffect: null,
740+
events: null,
730741
stores: null,
731742
memoCache: null,
732743
};
@@ -735,6 +746,7 @@ if (enableUseMemoCacheHook) {
735746
createFunctionComponentUpdateQueue = () => {
736747
return {
737748
lastEffect: null,
749+
events: null,
738750
stores: null,
739751
};
740752
};
@@ -1871,49 +1883,47 @@ function updateEffect(
18711883
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
18721884
}
18731885

1886+
function useEventImpl<T>(event: EventFunction<T>, nextEvent: () => T) {
1887+
const eventState = {event, nextEvent};
1888+
currentlyRenderingFiber.flags |= UpdateEffect;
1889+
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
1890+
if (componentUpdateQueue === null) {
1891+
componentUpdateQueue = createFunctionComponentUpdateQueue();
1892+
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1893+
componentUpdateQueue.events = [eventState];
1894+
} else {
1895+
const events = componentUpdateQueue.events;
1896+
if (events === null) {
1897+
componentUpdateQueue.events = [eventState];
1898+
} else {
1899+
events.push(eventState);
1900+
}
1901+
}
1902+
}
1903+
18741904
function mountEvent<T>(callback: () => T): () => T {
18751905
const hook = mountWorkInProgressHook();
1876-
const ref = {current: callback};
18771906

18781907
function event() {
18791908
if (isInvalidExecutionContextForEventFunction()) {
18801909
throw new Error(
18811910
"A function wrapped in useEvent can't be called during rendering.",
18821911
);
18831912
}
1884-
return ref.current.apply(undefined, arguments);
1913+
return event._current.apply(undefined, arguments);
18851914
}
1915+
event._current = callback;
18861916

1887-
// TODO: We don't need all the overhead of an effect object since there are no deps and no
1888-
// clean up functions.
1889-
mountEffectImpl(
1890-
UpdateEffect,
1891-
HookSnapshot,
1892-
() => {
1893-
ref.current = callback;
1894-
},
1895-
[ref, callback],
1896-
);
1897-
1898-
hook.memoizedState = [ref, event];
1899-
1917+
useEventImpl(event, callback);
1918+
hook.memoizedState = event;
19001919
return event;
19011920
}
19021921

19031922
function updateEvent<T>(callback: () => T): () => T {
19041923
const hook = updateWorkInProgressHook();
1905-
const ref = hook.memoizedState[0];
1906-
1907-
updateEffectImpl(
1908-
UpdateEffect,
1909-
HookSnapshot,
1910-
() => {
1911-
ref.current = callback;
1912-
},
1913-
[ref, callback],
1914-
);
1915-
1916-
return hook.memoizedState[1];
1924+
const event = hook.memoizedState;
1925+
useEventImpl(event, callback);
1926+
return event;
19171927
}
19181928

19191929
function mountInsertionEffect(

packages/react-reconciler/src/ReactFiberHooks.old.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ import {
8686
Layout as HookLayout,
8787
Passive as HookPassive,
8888
Insertion as HookInsertion,
89-
Snapshot as HookSnapshot,
9089
} from './ReactHookEffectTags';
9190
import {
9291
getWorkInProgressRoot,
@@ -182,8 +181,19 @@ type StoreConsistencyCheck<T> = {
182181
getSnapshot: () => T,
183182
};
184183

184+
type EventFunctionState<T> = {
185+
event: EventFunctionWrapper<T>,
186+
nextEvent: EventFunction<T>,
187+
};
188+
type EventFunctionWrapper<T> = {
189+
(): T,
190+
_current: EventFunction<T>,
191+
};
192+
type EventFunction<T> = () => T;
193+
185194
export type FunctionComponentUpdateQueue = {
186195
lastEffect: Effect | null,
196+
events: Array<EventFunctionState<any>> | null,
187197
stores: Array<StoreConsistencyCheck<any>> | null,
188198
// NOTE: optional, only set when enableUseMemoCacheHook is enabled
189199
memoCache?: MemoCache | null,
@@ -727,6 +737,7 @@ if (enableUseMemoCacheHook) {
727737
createFunctionComponentUpdateQueue = () => {
728738
return {
729739
lastEffect: null,
740+
events: null,
730741
stores: null,
731742
memoCache: null,
732743
};
@@ -735,6 +746,7 @@ if (enableUseMemoCacheHook) {
735746
createFunctionComponentUpdateQueue = () => {
736747
return {
737748
lastEffect: null,
749+
events: null,
738750
stores: null,
739751
};
740752
};
@@ -1871,49 +1883,47 @@ function updateEffect(
18711883
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
18721884
}
18731885

1886+
function useEventImpl<T>(event: EventFunction<T>, nextEvent: () => T) {
1887+
const eventState = {event, nextEvent};
1888+
currentlyRenderingFiber.flags |= UpdateEffect;
1889+
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
1890+
if (componentUpdateQueue === null) {
1891+
componentUpdateQueue = createFunctionComponentUpdateQueue();
1892+
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
1893+
componentUpdateQueue.events = [eventState];
1894+
} else {
1895+
const events = componentUpdateQueue.events;
1896+
if (events === null) {
1897+
componentUpdateQueue.events = [eventState];
1898+
} else {
1899+
events.push(eventState);
1900+
}
1901+
}
1902+
}
1903+
18741904
function mountEvent<T>(callback: () => T): () => T {
18751905
const hook = mountWorkInProgressHook();
1876-
const ref = {current: callback};
18771906

18781907
function event() {
18791908
if (isInvalidExecutionContextForEventFunction()) {
18801909
throw new Error(
18811910
"A function wrapped in useEvent can't be called during rendering.",
18821911
);
18831912
}
1884-
return ref.current.apply(undefined, arguments);
1913+
return event._current.apply(undefined, arguments);
18851914
}
1915+
event._current = callback;
18861916

1887-
// TODO: We don't need all the overhead of an effect object since there are no deps and no
1888-
// clean up functions.
1889-
mountEffectImpl(
1890-
UpdateEffect,
1891-
HookSnapshot,
1892-
() => {
1893-
ref.current = callback;
1894-
},
1895-
[ref, callback],
1896-
);
1897-
1898-
hook.memoizedState = [ref, event];
1899-
1917+
useEventImpl(event, callback);
1918+
hook.memoizedState = event;
19001919
return event;
19011920
}
19021921

19031922
function updateEvent<T>(callback: () => T): () => T {
19041923
const hook = updateWorkInProgressHook();
1905-
const ref = hook.memoizedState[0];
1906-
1907-
updateEffectImpl(
1908-
UpdateEffect,
1909-
HookSnapshot,
1910-
() => {
1911-
ref.current = callback;
1912-
},
1913-
[ref, callback],
1914-
);
1915-
1916-
return hook.memoizedState[1];
1924+
const event = hook.memoizedState;
1925+
useEventImpl(event, callback);
1926+
return event;
19171927
}
19181928

19191929
function mountInsertionEffect(

packages/react-reconciler/src/ReactHookEffectTags.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99

1010
export type HookFlags = number;
1111

12-
export const NoFlags = /* */ 0b00000;
12+
export const NoFlags = /* */ 0b0000;
1313

1414
// Represents whether effect should fire.
15-
export const HasEffect = /* */ 0b00001;
15+
export const HasEffect = /* */ 0b0001;
1616

1717
// Represents the phase in which the effect (not the clean-up) fires.
18-
export const Snapshot = /* */ 0b00010;
19-
export const Insertion = /* */ 0b00100;
20-
export const Layout = /* */ 0b01000;
21-
export const Passive = /* */ 0b10000;
18+
export const Insertion = /* */ 0b0010;
19+
export const Layout = /* */ 0b0100;
20+
export const Passive = /* */ 0b1000;

packages/react-reconciler/src/__tests__/useEvent-test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,62 @@ describe('useEvent', () => {
117117
]);
118118
});
119119

120+
// @gate enableUseEventHook
121+
it('can be defined more than once', () => {
122+
class IncrementButton extends React.PureComponent {
123+
increment = () => {
124+
this.props.onClick();
125+
};
126+
multiply = () => {
127+
this.props.onMouseEnter();
128+
};
129+
render() {
130+
return <Text text="Increment" />;
131+
}
132+
}
133+
134+
function Counter({incrementBy}) {
135+
const [count, updateCount] = useState(0);
136+
const onClick = useEvent(() => updateCount(c => c + incrementBy));
137+
const onMouseEnter = useEvent(() => {
138+
updateCount(c => c * incrementBy);
139+
});
140+
141+
return (
142+
<>
143+
<IncrementButton
144+
onClick={() => onClick()}
145+
onMouseEnter={() => onMouseEnter()}
146+
ref={button}
147+
/>
148+
<Text text={'Count: ' + count} />
149+
</>
150+
);
151+
}
152+
153+
const button = React.createRef(null);
154+
ReactNoop.render(<Counter incrementBy={5} />);
155+
expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
156+
expect(ReactNoop.getChildren()).toEqual([
157+
span('Increment'),
158+
span('Count: 0'),
159+
]);
160+
161+
act(button.current.increment);
162+
expect(Scheduler).toHaveYielded(['Increment', 'Count: 5']);
163+
expect(ReactNoop.getChildren()).toEqual([
164+
span('Increment'),
165+
span('Count: 5'),
166+
]);
167+
168+
act(button.current.multiply);
169+
expect(Scheduler).toHaveYielded(['Increment', 'Count: 25']);
170+
expect(ReactNoop.getChildren()).toEqual([
171+
span('Increment'),
172+
span('Count: 25'),
173+
]);
174+
});
175+
120176
// @gate enableUseEventHook
121177
it('does not preserve `this` in event functions', () => {
122178
class GreetButton extends React.PureComponent {

0 commit comments

Comments
 (0)