Skip to content

Commit

Permalink
fix(jsx/dom): should not return memoized result when context is chang…
Browse files Browse the repository at this point in the history
…ed (#3792)
  • Loading branch information
usualoma authored Jan 1, 2025
1 parent 1e62912 commit fa6f51b
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 1 deletion.
32 changes: 32 additions & 0 deletions src/jsx/dom/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import DefaultExport, {
cloneElement,
cloneElement as cloneElementForDom,
createElement as createElementForDom,
createContext,
useContext,
createPortal,
flushSync,
isValidElement,
Expand Down Expand Up @@ -1492,6 +1494,36 @@ describe('DOM', () => {
expect(root.innerHTML).toBe('<div><div><p>Count: 1</p></div><button>+</button></div>')
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 <Context.Provider value={[count, setCount]}>{children}</Context.Provider>
}
const Content = () => {
const [count, setCount] = useContext(Context)
return (
<>
<span>{count}</span>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</>
)
}
const app = (
<Container>
<Content />
</Container>
)
render(app, root)
expect(root.innerHTML).toBe('<span>0</span><button>+</button>')
root.querySelector('button')?.click()
await Promise.resolve()
expect(root.innerHTML).toBe('<span>1</span><button>+</button>')
})
})

describe('useRef', async () => {
Expand Down
14 changes: 13 additions & 1 deletion src/jsx/dom/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,16 @@ const applyNodeObject = (node: NodeObject, container: Container, isNew: boolean)
}
}

const isSameContext = (
oldContexts: LocalJSXContexts,
newContexts: NonNullable<LocalJSXContexts>
): boolean =>
!!(
oldContexts &&
oldContexts.length === newContexts.length &&
oldContexts.every((ctx, i) => ctx[1] === newContexts[i][1])
)

const fallbackUpdateFnArrayMap: WeakMap<
NodeObject,
Array<() => Promise<NodeObject | undefined>>
Expand Down Expand Up @@ -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<unknown>)[DOM_MEMO]?.(pP, oldChild.props)) // The `memo` function is memoized under this condition.
(oldChild.tag as MemorableFC<unknown>)[DOM_MEMO]?.(pP, oldChild.props)) && // The `memo` function is memoized under this condition.
isSameContext(oldContexts, oldChild[DOM_STASH][2])
) {
oldChild.s = true
}
Expand Down

0 comments on commit fa6f51b

Please sign in to comment.