Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/dull-baboons-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/solid-form': patch
---

prevent full array re-renders in array mode
2 changes: 1 addition & 1 deletion docs/framework/solid/guides/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function App() {
form.handleSubmit()
}}
>
<form.Field name="people">
<form.Field name="people" mode="array">
{(field) => (
<div>
<Show when={field().state.value.length > 0}>
Expand Down
2 changes: 1 addition & 1 deletion examples/solid/array/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function App() {
form.handleSubmit()
}}
>
<form.Field name="people">
<form.Field name="people" mode="array">
{(field) => (
<div>
<Show when={field().state.value.length > 0}>
Expand Down
55 changes: 49 additions & 6 deletions packages/solid-form/src/createField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import type {
} from '@tanstack/form-core'

import type { Accessor, JSX, JSXElement } from 'solid-js'
import type { CreateFieldOptions, CreateFieldOptionsBound } from './types'
import type {
CreateFieldOptions,
CreateFieldOptionsBound,
FieldOptionsMode,
} from './types'

interface SolidFieldApi<
TParentData,
Expand Down Expand Up @@ -123,6 +127,7 @@ function makeFieldReactive<
TFormOnServer,
TParentSubmitMeta
>,
{ mode }: FieldOptionsMode,
): () => FieldApi<
TParentData,
TName,
Expand Down Expand Up @@ -163,12 +168,50 @@ function makeFieldReactive<
TParentSubmitMeta
> {
const [field, setField] = createSignal(fieldApi, { equals: false })
// Handle shallow comparison to make sure that Derived doesn't create a new setField call every time
const store = useStore(fieldApi.store, (store) => store)
// Subscribe to the pieces of state that should trigger a re-render of the
// field. For array mode, we only track the length of the array value to
// avoid re-renders when child properties change. Meta is tracked piece by
// piece so that consumers re-render when any meta property updates.
// See: https://github.com/TanStack/form/issues/1961
const reactiveStateValue = useStore(fieldApi.store, (state) =>
mode === 'array'
? Object.keys((state.value as unknown) ?? []).length
: state.value,
)
const reactiveMetaIsTouched = useStore(
fieldApi.store,
(state) => state.meta.isTouched,
)
const reactiveMetaIsBlurred = useStore(
fieldApi.store,
(state) => state.meta.isBlurred,
)
const reactiveMetaIsDirty = useStore(
fieldApi.store,
(state) => state.meta.isDirty,
)
const reactiveMetaErrorMap = useStore(
fieldApi.store,
(state) => state.meta.errorMap,
)
const reactiveMetaErrorSourceMap = useStore(
fieldApi.store,
(state) => state.meta.errorSourceMap,
)
const reactiveMetaIsValidating = useStore(
fieldApi.store,
(state) => state.meta.isValidating,
)
// Run before initial render
createComputed(() => {
// Use the store to track dependencies
store()
// Read all reactive sources to track them as dependencies
reactiveStateValue()
reactiveMetaIsTouched()
reactiveMetaIsBlurred()
reactiveMetaIsDirty()
reactiveMetaErrorMap()
reactiveMetaErrorSourceMap()
reactiveMetaIsValidating()
setField(fieldApi)
})
return field
Expand Down Expand Up @@ -285,7 +328,7 @@ export function createField<
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
>(extendedApi as never)
>(extendedApi as never, { mode: options.mode })
}

interface FieldComponentBoundProps<
Expand Down
5 changes: 4 additions & 1 deletion packages/solid-form/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
FormValidateOrFn,
} from '@tanstack/form-core'

interface FieldOptionsMode {
/**
* @private
*/
export interface FieldOptionsMode {
mode?: 'value' | 'array'
}

Expand Down
28 changes: 28 additions & 0 deletions packages/solid-form/tests/createField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -567,4 +567,32 @@ describe('createField', () => {
await user.click(await findByText('Submit'))
expect(fn).toHaveBeenCalledWith({ people: [{ name: 'John', age: 0 }] })
})

it('should support array mode', async () => {
function Comp() {
const form = createForm(() => ({
defaultValues: {
test: ['a'],
},
}))

return (
<form.Field name="test" mode="array">
{(field) => (
<div>
<div data-testid="val">{JSON.stringify(field().state.value)}</div>
<button onClick={() => field().pushValue('b')}>push</button>
</div>
)}
</form.Field>
)
}

const { getByTestId, getByText } = render(() => <Comp />)
expect(getByTestId('val')).toHaveTextContent('["a"]')
await user.click(getByText('push'))
await waitFor(() =>
expect(getByTestId('val')).toHaveTextContent('["a","b"]'),
)
})
})
Loading