From a8a84f3055597e11ece9a1b1815c994acd4c675f Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 29 Jun 2024 17:24:39 +0900 Subject: [PATCH] feat(jsx/dom): skip calculate children if props are the same (#3049) * feat(jsx/dom): skip build children if props are the same * test: fix format * fix: re-calculate form element if state is updated --- src/jsx/dom/hooks/index.test.tsx | 27 ++++++++++----------- src/jsx/dom/index.test.tsx | 27 +++++++++++++++++++++ src/jsx/dom/intrinsic-element/components.ts | 11 ++++++--- src/jsx/dom/render.ts | 21 +++++++++++++--- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/jsx/dom/hooks/index.test.tsx b/src/jsx/dom/hooks/index.test.tsx index aeeb9996..4ce09a50 100644 --- a/src/jsx/dom/hooks/index.test.tsx +++ b/src/jsx/dom/hooks/index.test.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource ../../ */ import { JSDOM } from 'jsdom' -import { render, useState } from '..' +import { render, useCallback, useState } from '..' import { useActionState, useFormStatus, useOptimistic } from '.' describe('Hooks', () => { @@ -84,14 +84,13 @@ describe('Hooks', () => { } const App = () => { const [, setCount] = useState(0) + const action = useCallback(() => { + setCount((count) => count + 1) + return formPromise + }, []) return ( <> -
{ - setCount((count) => count + 1) - return formPromise - }} - > + @@ -134,15 +133,15 @@ describe('Hooks', () => { const App = () => { const [count, setCount] = useState(0) const [optimisticCount, setOptimisticCount] = useOptimistic(count, (c, n: number) => n) + const action = useCallback(async () => { + setOptimisticCount(count + 1) + await formPromise + setCount((count) => count + 2) + }, []) + return ( <> - { - setOptimisticCount(count + 1) - await formPromise - setCount((count) => count + 2) - }} - > +
{optimisticCount}
diff --git a/src/jsx/dom/index.test.tsx b/src/jsx/dom/index.test.tsx index 48c77cda..5b54c357 100644 --- a/src/jsx/dom/index.test.tsx +++ b/src/jsx/dom/index.test.tsx @@ -264,6 +264,33 @@ describe('DOM', () => { }) }) + describe('skip build child', () => { + it('simple', async () => { + const Child = vi.fn(({ count }: { count: number }) =>
{count}
) + const App = () => { + const [count, setCount] = useState(0) + return ( + <> +
{count}
+ + + + ) + } + render(, root) + expect(root.innerHTML).toBe('
0
0
') + expect(Child).toBeCalledTimes(1) + root.querySelector('button')?.click() + await Promise.resolve() + expect(root.innerHTML).toBe('
1
0
') + expect(Child).toBeCalledTimes(1) + root.querySelector('button')?.click() + await Promise.resolve() + expect(root.innerHTML).toBe('
2
1
') + expect(Child).toBeCalledTimes(2) + }) + }) + describe('defaultProps', () => { it('simple', () => { const App: FC<{ name?: string }> = ({ name }) =>
{name}
diff --git a/src/jsx/dom/intrinsic-element/components.ts b/src/jsx/dom/intrinsic-element/components.ts index 6806d155..81060f0d 100644 --- a/src/jsx/dom/intrinsic-element/components.ts +++ b/src/jsx/dom/intrinsic-element/components.ts @@ -277,7 +277,7 @@ export const form: FC< ;(restProps as any).action = action } - const [data, setData] = useState(null) + const [state, setState] = useState<[FormData | null, boolean]>([null, false]) // [FormData, isDirty] const onSubmit = useCallback<(ev: SubmitEvent | CustomEvent) => void>( async (ev: SubmitEvent | CustomEvent) => { const currentAction = ev.isTrusted @@ -289,13 +289,13 @@ export const form: FC< ev.preventDefault() const formData = new FormData(ev.target as HTMLFormElement) - setData(formData) + setState([formData, true]) const actionRes = currentAction(formData) if (actionRes instanceof Promise) { registerAction(actionRes) await actionRes } - setData(null) + setState([null, true]) }, [] ) @@ -307,6 +307,8 @@ export const form: FC< } }) + const [data, isDirty] = state + state[1] = false return newJSXNode({ tag: FormContext as unknown as Function, props: { @@ -324,8 +326,9 @@ export const form: FC< }, }), }, + f: isDirty, // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any + } as any) as any } const formActionableElement = ( diff --git a/src/jsx/dom/render.ts b/src/jsx/dom/render.ts index 8145235f..e51a72d3 100644 --- a/src/jsx/dom/render.ts +++ b/src/jsx/dom/render.ts @@ -43,6 +43,7 @@ export type NodeObject = { vR: Node[] // virtual dom children to remove s?: Node[] // shadow virtual dom children n?: string // namespace + f?: boolean // force build c: Container | undefined // container e: SupportedElement | Text | undefined // rendered element p?: PreserveNodeType // preserve HTMLElement if it will be unmounted @@ -444,6 +445,7 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v oldVChildren.splice(i, 1) } + let skipBuild = false if (oldChild) { if (isNodeString(child)) { if ((oldChild as NodeString).t !== child.t) { @@ -454,11 +456,20 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v } else if (oldChild.tag !== child.tag) { node.vR.push(oldChild) } else { - oldChild.pP = oldChild.props + const pP = (oldChild.pP = oldChild.props) oldChild.props = child.props + oldChild.f ||= child.f || node.f if (typeof child.tag === 'function') { oldChild[DOM_STASH][2] = child[DOM_STASH][2] || [] oldChild[DOM_STASH][3] = child[DOM_STASH][3] + + if (!oldChild.f) { + const prevPropsKeys = Object.keys(pP) + const currentProps = oldChild.props + skipBuild = + prevPropsKeys.length === Object.keys(currentProps).length && + prevPropsKeys.every((k) => k in currentProps && currentProps[k] === pP[k]) + } } child = oldChild } @@ -469,8 +480,9 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v } } - if (!isNodeString(child)) { + if (!isNodeString(child) && !skipBuild) { build(context, child) + delete child.f } vChildren.push(child) @@ -486,6 +498,7 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v delete node.pC } } catch (e) { + node.f = true if (e === cancelBuild) { if (foundErrorHandler) { return @@ -552,7 +565,9 @@ export const buildNode = (node: Child): Node | undefined => { tag: (node as NodeObject).tag, props: (node as NodeObject).props, key: (node as NodeObject).key, - }) + f: (node as NodeObject).f, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) } if (typeof (node as JSXNode).tag === 'function') { ;(node as NodeObject)[DOM_STASH] = [0, []]