Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsx/dom): skip calculate children if props are the same #3049

Merged
merged 3 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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