Skip to content

Commit

Permalink
feat(jsx/dom): skip calculate children if props are the same (#3049)
Browse files Browse the repository at this point in the history
* feat(jsx/dom): skip build children if props are the same

* test: fix format

* fix: re-calculate form element if state is updated
  • Loading branch information
usualoma authored Jun 29, 2024
1 parent cb79f23 commit a8a84f3
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 21 deletions.
27 changes: 13 additions & 14 deletions src/jsx/dom/hooks/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -84,14 +84,13 @@ describe('Hooks', () => {
}
const App = () => {
const [, setCount] = useState(0)
const action = useCallback(() => {
setCount((count) => count + 1)
return formPromise
}, [])
return (
<>
<form
action={() => {
setCount((count) => count + 1)
return formPromise
}}
>
<form action={action}>
<Status />
<input type='text' name='name' value='updated' />
<button>Submit</button>
Expand Down Expand Up @@ -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 (
<>
<form
action={async () => {
setOptimisticCount(count + 1)
await formPromise
setCount((count) => count + 2)
}}
>
<form action={action}>
<div>{optimisticCount}</div>
<input type='text' name='name' value='updated' />
<button>Submit</button>
Expand Down
27 changes: 27 additions & 0 deletions src/jsx/dom/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,33 @@ describe('DOM', () => {
})
})

describe('skip build child', () => {
it('simple', async () => {
const Child = vi.fn(({ count }: { count: number }) => <div>{count}</div>)
const App = () => {
const [count, setCount] = useState(0)
return (
<>
<div>{count}</div>
<Child count={Math.floor(count / 2)} />
<button onClick={() => setCount(count + 1)}>+</button>
</>
)
}
render(<App />, root)
expect(root.innerHTML).toBe('<div>0</div><div>0</div><button>+</button>')
expect(Child).toBeCalledTimes(1)
root.querySelector('button')?.click()
await Promise.resolve()
expect(root.innerHTML).toBe('<div>1</div><div>0</div><button>+</button>')
expect(Child).toBeCalledTimes(1)
root.querySelector('button')?.click()
await Promise.resolve()
expect(root.innerHTML).toBe('<div>2</div><div>1</div><button>+</button>')
expect(Child).toBeCalledTimes(2)
})
})

describe('defaultProps', () => {
it('simple', () => {
const App: FC<{ name?: string }> = ({ name }) => <div>{name}</div>
Expand Down
11 changes: 7 additions & 4 deletions src/jsx/dom/intrinsic-element/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export const form: FC<
;(restProps as any).action = action
}

const [data, setData] = useState<FormData | null>(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
Expand All @@ -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])
},
[]
)
Expand All @@ -307,6 +307,8 @@ export const form: FC<
}
})

const [data, isDirty] = state
state[1] = false
return newJSXNode({
tag: FormContext as unknown as Function,
props: {
Expand All @@ -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 = (
Expand Down
21 changes: 18 additions & 3 deletions src/jsx/dom/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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, []]
Expand Down

0 comments on commit a8a84f3

Please sign in to comment.