From 5f945e421bc2c10e94fd767fcaa20b987dae34db Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 10 May 2026 16:51:11 -0400 Subject: [PATCH 1/3] fix(solid-form): prevent full array re-renders in array mode --- docs/framework/solid/guides/arrays.md | 2 +- examples/solid/array/src/index.tsx | 2 +- packages/solid-form/src/createField.tsx | 57 ++++++++++++++++--- packages/solid-form/src/types.ts | 5 +- .../solid-form/tests/createField.test.tsx | 28 +++++++++ 5 files changed, 84 insertions(+), 10 deletions(-) diff --git a/docs/framework/solid/guides/arrays.md b/docs/framework/solid/guides/arrays.md index 52d9149b6..8c1b5240e 100644 --- a/docs/framework/solid/guides/arrays.md +++ b/docs/framework/solid/guides/arrays.md @@ -83,7 +83,7 @@ function App() { form.handleSubmit() }} > - + {(field) => (
0}> diff --git a/examples/solid/array/src/index.tsx b/examples/solid/array/src/index.tsx index e6beadce9..e930e712c 100644 --- a/examples/solid/array/src/index.tsx +++ b/examples/solid/array/src/index.tsx @@ -21,7 +21,7 @@ function App() { form.handleSubmit() }} > - + {(field) => (
0}> diff --git a/packages/solid-form/src/createField.tsx b/packages/solid-form/src/createField.tsx index 1284f0aaf..219c4ee19 100644 --- a/packages/solid-form/src/createField.tsx +++ b/packages/solid-form/src/createField.tsx @@ -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, @@ -122,7 +126,8 @@ function makeFieldReactive< TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta - >, + > & + FieldOptionsMode, ): () => FieldApi< TParentData, TName, @@ -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 @@ -285,7 +328,7 @@ export function createField< TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta - >(extendedApi as never) + >(extendedApi as never, options.mode) } interface FieldComponentBoundProps< diff --git a/packages/solid-form/src/types.ts b/packages/solid-form/src/types.ts index 21909578e..2d0807655 100644 --- a/packages/solid-form/src/types.ts +++ b/packages/solid-form/src/types.ts @@ -9,7 +9,10 @@ import type { FormValidateOrFn, } from '@tanstack/form-core' -interface FieldOptionsMode { +/** + * @private + */ +export interface FieldOptionsMode { mode?: 'value' | 'array' } diff --git a/packages/solid-form/tests/createField.test.tsx b/packages/solid-form/tests/createField.test.tsx index df2b444a1..a2a492010 100644 --- a/packages/solid-form/tests/createField.test.tsx +++ b/packages/solid-form/tests/createField.test.tsx @@ -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 ( + + {(field) => ( +
+
{JSON.stringify(field().state.value)}
+ +
+ )} +
+ ) + } + + const { getByTestId, getByText } = render(() => ) + expect(getByTestId('val')).toHaveTextContent('["a"]') + await user.click(getByText('push')) + await waitFor(() => + expect(getByTestId('val')).toHaveTextContent('["a","b"]'), + ) + }) }) From 8c4f75dede42cee311b5760fbb9806732120ddbb Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 10 May 2026 16:53:01 -0400 Subject: [PATCH 2/3] chore: add changeset --- .changeset/dull-baboons-like.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-baboons-like.md diff --git a/.changeset/dull-baboons-like.md b/.changeset/dull-baboons-like.md new file mode 100644 index 000000000..bdc52fae0 --- /dev/null +++ b/.changeset/dull-baboons-like.md @@ -0,0 +1,5 @@ +--- +'@tanstack/solid-form': patch +--- + +prevent full array re-renders in array mode From 3438a777c2f93ca3d9f40c859752bbfcb1d597e5 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 10 May 2026 17:00:16 -0400 Subject: [PATCH 3/3] chore: fix build --- packages/solid-form/src/createField.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/solid-form/src/createField.tsx b/packages/solid-form/src/createField.tsx index 219c4ee19..18ff73c70 100644 --- a/packages/solid-form/src/createField.tsx +++ b/packages/solid-form/src/createField.tsx @@ -126,8 +126,8 @@ function makeFieldReactive< TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta - > & - FieldOptionsMode, + >, + { mode }: FieldOptionsMode, ): () => FieldApi< TParentData, TName, @@ -328,7 +328,7 @@ export function createField< TFormOnDynamicAsync, TFormOnServer, TParentSubmitMeta - >(extendedApi as never, options.mode) + >(extendedApi as never, { mode: options.mode }) } interface FieldComponentBoundProps<