mirror of
https://github.com/honojs/hono.git
synced 2024-11-21 18:18:57 +01:00
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
This commit is contained in:
parent
cb79f2302b
commit
a8a84f3055
@ -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 (
|
||||
<>
|
||||
<form
|
||||
action={() => {
|
||||
setCount((count) => count + 1)
|
||||
return formPromise
|
||||
}}
|
||||
>
|
||||
<form action={action}>
|
||||
<Status />
|
||||
<input type='text' name='name' value='updated' />
|
||||
<button>Submit</button>
|
||||
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
@ -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 = (
|
||||
|
@ -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, []]
|
||||
|
Loading…
Reference in New Issue
Block a user