From 92213056d883952085b217ba4806f55dec002877 Mon Sep 17 00:00:00 2001 From: wmzy <1256573276@qq.com> Date: Thu, 9 Feb 2023 05:48:03 +0800 Subject: [PATCH 1/3] RFC: Extends `memo` function to support mutable event handlers. --- text/0000-extends-memo-for-event-handler.md | 122 ++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 text/0000-extends-memo-for-event-handler.md diff --git a/text/0000-extends-memo-for-event-handler.md b/text/0000-extends-memo-for-event-handler.md new file mode 100644 index 00000000..491d06ae --- /dev/null +++ b/text/0000-extends-memo-for-event-handler.md @@ -0,0 +1,122 @@ +- Start Date: 2023-02-08 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +Extends `memo` function to support mutable event handlers. + +# Basic example + +You need not wrap any event handler by `useCallback` or `useEvent`. + +```js +import OriginalSendButton from 'some-where'; + +let SendButton = OriginalSendButton; + +// When you want to optimize `SendButton`, just memoization like: +SendButton = memo(SendButton, {events: true}); + +function Chat() { + const [text, setText] = useState(''); + + const onClick = () => { + sendMessage(text); + }; + + return ; +} +``` + +# Motivation + +Make `memo` work for mutable event handlers. + +It is similar with `useEvent` rfc [first motivation](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#reading-stateprops-in-event-handlers-breaks-optimizations). + +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: + +```jsx +// (!) Approximate behavior +import {memo as reactMemo, useRef} from 'react'; + +export function memo(Component, options) { + if (!options || typeof options === 'function') { + return reactMemo(Component, options); + } + + const {events, propsAreEqual} = options; + + if (options.events) { + + function Wrapped(props) { + const memoEvents = useRef({}).current; + const eventNames = Array.isArray(events) + ? events + : Object.keys(props).filter((k) => /^on[A-Z]/.test(k)); + for (const e of eventNames) { + memoEvents[e] = memoEvents[e] || wrapEvent(props[e]); + } + return ; + } + + return reactMemo(Wrapped); + } + + return reactMemo(Component, propsAreEqual); +} + +const ws = new WeakSet(); + +function wrapEvent(func) { + if (typeof func !== 'function' || ws.has(func)) return func; + const wrapped = (...params) => func(...params); + ws.add(wrapped); + return wrapped; +} +``` + +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 `PureComponent` and `shouldComponentUpdate`. From f5bdb995c8073937a69eac23cdd8e47c4c7ae182 Mon Sep 17 00:00:00 2001 From: wmzy <1256573276@qq.com> Date: Wed, 31 May 2023 11:23:02 +0800 Subject: [PATCH 2/3] fix: internal implementation --- text/0000-extends-memo-for-event-handler.md | 61 +++++++++++++-------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/text/0000-extends-memo-for-event-handler.md b/text/0000-extends-memo-for-event-handler.md index 491d06ae..856c53ea 100644 --- a/text/0000-extends-memo-for-event-handler.md +++ b/text/0000-extends-memo-for-event-handler.md @@ -49,42 +49,59 @@ Internally, `memo` function will approximately work like this: ```jsx // (!) Approximate behavior -import {memo as reactMemo, useRef} from 'react'; +import {memo as reactMemo, forwardRef, useRef} from 'react'; -export function memo(Component, options) { +export default function memo(WrappedComponent, options) { if (!options || typeof options === 'function') { - return reactMemo(Component, options); + return reactMemo(WrappedComponent, options); } - const {events, propsAreEqual} = options; - - if (options.events) { - - function Wrapped(props) { - const memoEvents = useRef({}).current; + let Wrapped = WrappedComponent; + const {events} = options; + if (events) { + // events is an array or true + Wrapped = forwardRef((props, ref) => { + const memoEventsRef = useRef({}); const eventNames = Array.isArray(events) ? events : Object.keys(props).filter((k) => /^on[A-Z]/.test(k)); - for (const e of eventNames) { - memoEvents[e] = memoEvents[e] || wrapEvent(props[e]); - } - return ; - } - return reactMemo(Wrapped); + const memoEvents = eventNames.reduce( + (me, n) => ({ + ...me, + [n]: fixEventHandler(memoEventsRef.current[e], props[e]) + }), + {} + ); + memoEventsRef.current = memoEvents; + return ; + }); + + Wrapped.displayName = `FixedEvent(${getDisplayName(WrappedComponent)})`; } - return reactMemo(Component, propsAreEqual); + return reactMemo(Wrapped, options.propsAreEqual); } -const ws = new WeakSet(); +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); + }; + } -function wrapEvent(func) { - if (typeof func !== 'function' || ws.has(func)) return func; - const wrapped = (...params) => func(...params); - ws.add(wrapped); - return wrapped; + 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. From a36e28f71239b4aebcc1d705aa2a86cdf98d3f93 Mon Sep 17 00:00:00 2001 From: wmzy <1256573276@qq.com> Date: Wed, 31 May 2023 20:32:42 +0800 Subject: [PATCH 3/3] fix: memo order --- text/0000-extends-memo-for-event-handler.md | 48 +++++++++++---------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/text/0000-extends-memo-for-event-handler.md b/text/0000-extends-memo-for-event-handler.md index 856c53ea..150d1209 100644 --- a/text/0000-extends-memo-for-event-handler.md +++ b/text/0000-extends-memo-for-event-handler.md @@ -56,31 +56,33 @@ export default function memo(WrappedComponent, options) { return reactMemo(WrappedComponent, options); } - let Wrapped = WrappedComponent; + const MemoComponent = reactMemo(Wrapped, options.propsAreEqual); + const {events} = options; - if (events) { - // events is an array or true - 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 ; - }); - - Wrapped.displayName = `FixedEvent(${getDisplayName(WrappedComponent)})`; - } - return reactMemo(Wrapped, options.propsAreEqual); + 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 ; + }); + + Wrapped.displayName = `FixedEvent(${getDisplayName(MemoComponent)})`; + + return Wrapped; } const wm = new WeakMap();