Extends memo function to support mutable event handlers.
Basic example
You need not wrap any event handler by useCallback or useEvent.
;
let SendButton = OriginalSendButton;
// When you want to optimize `SendButton`, just memoization like:
SendButton = ;
Motivation
Make memo work for mutable event handlers.
It is similar with useEvent rfc first motivation.
Before memo will break by mutable event handlers.
So a lot of people wrap every event handler with useCallback or useEvent.
The wrapper is annoying, and most of the time it just makes performance worse.
Detailed design
Internal implementation
Internally, memo function will approximately work like this:
// (!) Approximate behavior
import {memo as reactMemo, forwardRef, useRef} from 'react';
export default function memo(WrappedComponent, options) {
if (!options || typeof options === 'function') {
return reactMemo(WrappedComponent, options);
}
const MemoComponent = reactMemo(Wrapped, options.propsAreEqual);
const {events} = options;
if (!events) return MemoComponent;
// events is an array or true
const Wrapped = forwardRef((props, ref) => {
const memoEventsRef = useRef({});
const eventNames = Array.isArray(events)
? events
: Object.keys(props).filter((k) => /^on[A-Z]/.test(k));
const memoEvents = eventNames.reduce(
(me, n) => ({
...me,
[n]: fixEventHandler(memoEventsRef.current[e], props[e])
}),
{}
);
memoEventsRef.current = memoEvents;
return <MemoComponent ref={ref} {...props} {...memoEvents} />;
});
Wrapped.displayName = `FixedEvent(${getDisplayName(MemoComponent)})`;
return Wrapped;
}
const wm = new WeakMap();
function fixEventHandler(fixed, handler) {
if (typeof handler !== 'function' || wm.has(handler)) return handler;
if (!fixed) {
fixed = (...params) => {
const h = wm.get(fixed);
return h(...params);
};
}
wm.set(fixed, handler);
return fixed;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
IMHO, the essence of the problem is that the event handler should not be regarded as an ordinary callback. So we need to add a proxy layer to the event as above.
Drawbacks
- If the event props does not follow the naming convention, it will cause unnecessary wrapping.
- There will be some performance overhead for detecting event props every render.
- In order to be compatible with the original behavior, the default parameter settings may not be reasonable.
Alternatives
useCallback, useEvent and some other wrapper hooks put too much mental load on the user.
User may don not know when to use or not. This can easily lead to premature optimization and reverse optimization.
Adoption strategy
Release it in a minor.
Use the linter to advise users to remove useCallback wrappers for functions starting with on* or handle*.
Write new documentation teaching common patterns.
Warn for memo without events option.
How we teach this
In the memo documentation, recommend to use this feature.
In the useCallback documentation, emphasis callback and event handler are two different concepts.
Unresolved questions
- How to teach and warn people not use event handler as callback on render time?
- How to add the similar behavior to
PureComponentandshouldComponentUpdate.