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