From fa6f51bb1b110cddc7dc41cb7e531fab27fe871f Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Wed, 1 Jan 2025 13:34:26 +0900 Subject: [PATCH] fix(jsx/dom): should not return memoized result when context is changed (#3792) --- src/jsx/dom/index.test.tsx | 32 ++++++++++++++++++++++++++++++++ src/jsx/dom/render.ts | 14 +++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/jsx/dom/index.test.tsx b/src/jsx/dom/index.test.tsx index 03e552fe8..23a4f82eb 100644 --- a/src/jsx/dom/index.test.tsx +++ b/src/jsx/dom/index.test.tsx @@ -19,6 +19,8 @@ import DefaultExport, { cloneElement, cloneElement as cloneElementForDom, createElement as createElementForDom, + createContext, + useContext, createPortal, flushSync, isValidElement, @@ -1492,6 +1494,36 @@ describe('DOM', () => { expect(root.innerHTML).toBe('

Count: 1

') expect(renderCount).toBe(2) }) + + it('should not return memoized result when context is not changed', async () => { + const Context = createContext<[number, (arg: number | ((value: number) => number)) => void]>([ + 0, + () => {}, + ]) + const Container: FC<{ children: Child }> = ({ children }) => { + const [count, setCount] = useState(0) + return {children} + } + const Content = () => { + const [count, setCount] = useContext(Context) + return ( + <> + {count} + + + ) + } + const app = ( + + + + ) + render(app, root) + expect(root.innerHTML).toBe('0') + root.querySelector('button')?.click() + await Promise.resolve() + expect(root.innerHTML).toBe('1') + }) }) describe('useRef', async () => { diff --git a/src/jsx/dom/render.ts b/src/jsx/dom/render.ts index 2297c3af3..ecaaaa626 100644 --- a/src/jsx/dom/render.ts +++ b/src/jsx/dom/render.ts @@ -451,6 +451,16 @@ const applyNodeObject = (node: NodeObject, container: Container, isNew: boolean) } } +const isSameContext = ( + oldContexts: LocalJSXContexts, + newContexts: NonNullable +): boolean => + !!( + oldContexts && + oldContexts.length === newContexts.length && + oldContexts.every((ctx, i) => ctx[1] === newContexts[i][1]) + ) + const fallbackUpdateFnArrayMap: WeakMap< NodeObject, Array<() => Promise> @@ -525,13 +535,15 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v oldChild.props = child.props oldChild.f ||= child.f || node.f if (typeof child.tag === 'function') { + const oldContexts = oldChild[DOM_STASH][2] oldChild[DOM_STASH][2] = child[DOM_STASH][2] || [] oldChild[DOM_STASH][3] = child[DOM_STASH][3] if ( !oldChild.f && ((oldChild.o || oldChild) === child.o || // The code generated by the react compiler is memoized under this condition. - (oldChild.tag as MemorableFC)[DOM_MEMO]?.(pP, oldChild.props)) // The `memo` function is memoized under this condition. + (oldChild.tag as MemorableFC)[DOM_MEMO]?.(pP, oldChild.props)) && // The `memo` function is memoized under this condition. + isSameContext(oldContexts, oldChild[DOM_STASH][2]) ) { oldChild.s = true }