From 0542bec3253eb3a434f35bfd2f69ee4df46ffcd9 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Mon, 29 Apr 2024 21:10:02 +0900 Subject: [PATCH] breaking: make it compatible with memo and react compiler (#202) * reuse affected if the state is not changed from the previous one * update snapshot * per-hook affected * remove memo * update docs * v2.0.0-beta.1 * v2.0.0-beta.2 * v2.0.0-beta.3 * update CHANGELOG --- CHANGELOG.md | 4 +++ .../e2e/__snapshots__/14_dynamic.ts.snap | 14 ++++---- examples/09_reactmemo/src/TodoItem.tsx | 4 +-- examples/15_reactmemoref/src/TodoItem.tsx | 6 ++-- package.json | 5 ++- src/createTrackedSelector.ts | 10 +++--- src/index.ts | 1 - src/memo.ts | 34 ------------------- website/docs/api.md | 5 ++- website/docs/caveats.md | 4 ++- website/docs/tutorial-02.md | 7 ++-- website/docs/tutorial-03.md | 7 ++-- 12 files changed, 34 insertions(+), 67 deletions(-) delete mode 100644 src/memo.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d95e97c6..6194056e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Change Log ## [Unreleased] +### Added +- breaking: make it compatible with memo and react compiler #202 +### Removed +- `memo` is removed as it is no longer necessary with #202 ## [1.7.14] - 2024-03-08 ### Changed diff --git a/__tests__/e2e/__snapshots__/14_dynamic.ts.snap b/__tests__/e2e/__snapshots__/14_dynamic.ts.snap index 5f72ddff..acb0004b 100644 --- a/__tests__/e2e/__snapshots__/14_dynamic.ts.snap +++ b/__tests__/e2e/__snapshots__/14_dynamic.ts.snap @@ -42,7 +42,7 @@ exports[`14_dynamic should work with recorded events 5`] = ` exports[`14_dynamic should work with recorded events 6`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 2
First Name:
Age:
numRendered: 4
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 2
First Name:
Age:
numRendered: 4
First Name:
Age:
" @@ -50,7 +50,7 @@ exports[`14_dynamic should work with recorded events 6`] = ` exports[`14_dynamic should work with recorded events 7`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 2
First Name:
Age:
numRendered: 4
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 2
First Name:
Age:
numRendered: 4
First Name:
Age:
" @@ -58,7 +58,7 @@ exports[`14_dynamic should work with recorded events 7`] = ` exports[`14_dynamic should work with recorded events 8`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 34
First Name:
Age:
numRendered: 36
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 34
First Name:
Age:
numRendered: 36
First Name:
Age:
" @@ -66,7 +66,7 @@ exports[`14_dynamic should work with recorded events 8`] = ` exports[`14_dynamic should work with recorded events 9`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 38
Last Name:
Age:
numRendered: 36
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 38
Last Name:
Age:
numRendered: 36
First Name:
Age:
" @@ -74,7 +74,7 @@ exports[`14_dynamic should work with recorded events 9`] = ` exports[`14_dynamic should work with recorded events 10`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 38
Last Name:
Age:
numRendered: 36
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 38
Last Name:
Age:
numRendered: 36
First Name:
Age:
" @@ -82,7 +82,7 @@ exports[`14_dynamic should work with recorded events 10`] = ` exports[`14_dynamic should work with recorded events 11`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 44
Last Name:
Age:
numRendered: 36
First Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 44
Last Name:
Age:
numRendered: 36
First Name:
Age:
" @@ -90,7 +90,7 @@ exports[`14_dynamic should work with recorded events 11`] = ` exports[`14_dynamic should work with recorded events 12`] = ` " -

Counter

numRendered: 28
Count: -1
numRendered: 36
Count: -1

Person

numRendered: 44
Last Name:
Age:
numRendered: 52
Last Name:
Age:
+

Counter

numRendered: 28
Count: -1
numRendered: 33
Count: -1

Person

numRendered: 44
Last Name:
Age:
numRendered: 49
Last Name:
Age:
" diff --git a/examples/09_reactmemo/src/TodoItem.tsx b/examples/09_reactmemo/src/TodoItem.tsx index 6e353cbe..afafca63 100644 --- a/examples/09_reactmemo/src/TodoItem.tsx +++ b/examples/09_reactmemo/src/TodoItem.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { memo } from 'react-tracked'; import { useDispatch, TodoType } from './store'; @@ -9,8 +8,7 @@ type Props = { let numRendered = 0; -// Use custom memo instead of React.memo -const TodoItem = memo(({ todo }: Props) => { +const TodoItem = React.memo(({ todo }: Props) => { const dispatch = useDispatch(); return (
  • diff --git a/examples/15_reactmemoref/src/TodoItem.tsx b/examples/15_reactmemoref/src/TodoItem.tsx index 5a8baea6..68b92a4a 100644 --- a/examples/15_reactmemoref/src/TodoItem.tsx +++ b/examples/15_reactmemoref/src/TodoItem.tsx @@ -1,16 +1,16 @@ import React, { forwardRef } from 'react'; -import { memo } from 'react-tracked'; import { useDispatch, TodoType } from './store'; type Props = { + // FIXME why this complaints? + // eslint-disable-next-line react/no-unused-prop-types todo: TodoType; }; let numRendered = 0; -// Use custom memo instead of React.memo -const TodoItem = memo( +const TodoItem = React.memo( forwardRef(({ todo }, ref) => { const dispatch = useDispatch(); return ( diff --git a/package.json b/package.json index daa11ce0..b5c31305 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "react-tracked", "description": "State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.", - "version": "1.7.14", + "version": "2.0.0-beta.3", + "publishConfig": { + "tag": "next" + }, "author": "Daishi Kato", "repository": { "type": "git", diff --git a/src/createTrackedSelector.ts b/src/createTrackedSelector.ts index 35ce002d..c0de0668 100644 --- a/src/createTrackedSelector.ts +++ b/src/createTrackedSelector.ts @@ -14,12 +14,11 @@ export const createTrackedSelector = ( ) => { const useTrackedSelector = () => { const [, forceUpdate] = useReducer((c) => c + 1, 0); - const affected = new WeakMap(); - const lastAffected = useRef(); + // per-hook affected, it's not ideal but memo compatible + const affected = useMemo(() => new WeakMap(), []); const prevState = useRef(); const lastState = useRef(); useEffect(() => { - lastAffected.current = affected; if (prevState.current !== lastState.current && isChanged( prevState.current, @@ -35,11 +34,10 @@ export const createTrackedSelector = ( lastState.current = nextState; if (prevState.current && prevState.current !== nextState - && lastAffected.current && !isChanged( prevState.current, nextState, - lastAffected.current, + affected, new WeakMap(), ) ) { @@ -48,7 +46,7 @@ export const createTrackedSelector = ( } prevState.current = nextState; return nextState; - }, []); + }, [affected]); const state = useSelector(selector); if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/src/index.ts b/src/index.ts index 4993bf39..97ae345e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ export { createContainer } from './createContainer'; export { createTrackedSelector } from './createTrackedSelector'; -export { memo } from './memo'; export { getUntracked as getUntrackedObject } from 'proxy-compare'; diff --git a/src/memo.ts b/src/memo.ts deleted file mode 100644 index 14068eab..00000000 --- a/src/memo.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createElement, memo as reactMemo, forwardRef } from 'react'; -import { trackMemo } from 'proxy-compare'; - -import type { - PropsWithChildren, - NamedExoticComponent, - ComponentType, - ComponentProps, - MemoExoticComponent, -} from 'react'; - -export function memo

    >( - Component: ComponentType

    , - propsAreEqual?: ( - prevProps: Readonly>, - nextProps: Readonly>, - ) => boolean, -): NamedExoticComponent

    ; - -export function memo>( - Component: T, - propsAreEqual?: ( - prevProps: Readonly>, - nextProps: Readonly>, - ) => boolean, -): MemoExoticComponent; - -export function memo(Component: any, propsAreEqual?: any) { - const WrappedComponent = forwardRef((props: any, ref: any) => { - Object.values(props).forEach(trackMemo); - return createElement(Component, { ...props, ref }); - }); - return reactMemo(WrappedComponent, propsAreEqual); -} diff --git a/website/docs/api.md b/website/docs/api.md index 35897eb6..0c65cdb6 100644 --- a/website/docs/api.md +++ b/website/docs/api.md @@ -147,6 +147,9 @@ const Component = () => { ## memo +`memo` is removed in v2. With v2, we can use `React.memo`. +The description below only applies to v1. + There is a utility function exported from the library. This should be used instead of `React.memo` if props @@ -155,7 +158,7 @@ work correctly because a memoized component doesn't always render when a parent component renders. ```javascript -import { memo } from 'react-tracked'; +import { memo } from 'react-tracked'; // v1 only const ChildComponent = memo(({ num1, str1, obj1, obj2 }) => { // ... diff --git a/website/docs/caveats.md b/website/docs/caveats.md index e368d93d..a27d4d50 100644 --- a/website/docs/caveats.md +++ b/website/docs/caveats.md @@ -19,7 +19,9 @@ const state2 = useTrackedState(); You should use `useTrackedState` only once in a component if you need referential equality of objects in the state. -## An object referential change doesn't trigger re-render if an property of the object is accessed in previous render +## An object referential change doesn't trigger re-render if an property of the object is accessed in previous render (v1 only) + +:warning: This caveat only applies to v1. Since v2, we don't have such limitations. ```javascript const state = useTrackedState(); diff --git a/website/docs/tutorial-02.md b/website/docs/tutorial-02.md index 586913cd..28c016ba 100644 --- a/website/docs/tutorial-02.md +++ b/website/docs/tutorial-02.md @@ -192,11 +192,8 @@ export default React.memo(TodoItem); ``` This is the TodoItem component. -We prefer primitive props for memoized components. - -If you want to use object props for memoized components, -you need to use a special [memo](../api#memo) instead of `React.memo`. -See [example/09](https://github.com/dai-shi/react-tracked/tree/main/examples/09_reactmemo) for the usage. +We used to prefer primitive props for memoized components with v1. +With v2, object props are also fine. ## src/NewTodo.js diff --git a/website/docs/tutorial-03.md b/website/docs/tutorial-03.md index 81796804..9a3b3dc4 100644 --- a/website/docs/tutorial-03.md +++ b/website/docs/tutorial-03.md @@ -278,11 +278,8 @@ export default React.memo(TodoItem); ``` This is the TodoItem component. -We prefer primitive props for memoized components. - -If you want to use object props for memoized components, -you need to use a special [memo](../api#memo) instead of `React.memo`. -See [example/09](https://github.com/dai-shi/react-tracked/tree/main/examples/09_reactmemo) for the usage. +We used to prefer primitive props for memoized components with v1. +With v2, object props are also fine. ## src/components/NewTodo.js