diff --git a/.changeset/public-knives-dress.md b/.changeset/public-knives-dress.md new file mode 100644 index 000000000..1e682b240 --- /dev/null +++ b/.changeset/public-knives-dress.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-form': patch +--- + +prevent full array re-renders in array mode diff --git a/docs/framework/svelte/guides/arrays.md b/docs/framework/svelte/guides/arrays.md index 40acc6be7..8684726e5 100644 --- a/docs/framework/svelte/guides/arrays.md +++ b/docs/framework/svelte/guides/arrays.md @@ -75,7 +75,7 @@ Finally, you can use a subfield like so: form.handleSubmit() }} > - + {#snippet children(field)}
{#each field.state.value as person, i} diff --git a/examples/svelte/array/src/App.svelte b/examples/svelte/array/src/App.svelte index 4d8c02dcf..765bda30e 100644 --- a/examples/svelte/array/src/App.svelte +++ b/examples/svelte/array/src/App.svelte @@ -19,7 +19,7 @@ >

TanStack Form - Svelte Demo

- + {#snippet children(field)}
{#each field.state.value as person, i} diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index 9d63c23e4..8300a6fbb 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -96,10 +96,59 @@ api.update(current) }) - const storeSub = useStore(api.store) + const storeSub = useStore(api.store, (state) => + options.mode === 'array' + ? Object.keys((state.value as unknown) ?? []).length + : state.value, + ) + const metaIsTouchedSub = useStore( + api.store, + (state) => state.meta.isTouched, + ) + const metaIsBlurredSub = useStore( + api.store, + (state) => state.meta.isBlurred, + ) + const metaIsDirtySub = useStore(api.store, (state) => state.meta.isDirty) + const metaErrorMapSub = useStore(api.store, (state) => state.meta.errorMap) + const metaErrorSourceMapSub = useStore( + api.store, + (state) => state.meta.errorSourceMap, + ) + const metaIsValidatingSub = useStore( + api.store, + (state) => state.meta.isValidating, + ) Object.defineProperty(extendedApi, 'state', { get() { - return storeSub.current + // Read all reactive sources to track them as dependencies. For array + // mode, `storeSub.current` is the array length so we still pull the + // actual value from the underlying api state. + // See: https://github.com/TanStack/form/issues/1961 + // Note: we read from `api.store.state` (not `api.state`) to avoid + // infinite recursion since this getter shadows the prototype's `state` + // getter on the same object. + const trackedValue = storeSub.current + const isTouched = metaIsTouchedSub.current + const isBlurred = metaIsBlurredSub.current + const isDirty = metaIsDirtySub.current + const errorMap = metaErrorMapSub.current + const errorSourceMap = metaErrorSourceMapSub.current + const isValidating = metaIsValidatingSub.current + const baseState = api.store.state + return { + ...baseState, + value: options.mode === 'array' ? baseState.value : trackedValue, + meta: { + ...baseState.meta, + isTouched, + isBlurred, + isDirty, + errorMap, + errorSourceMap, + isValidating, + }, + } }, }) diff --git a/packages/svelte-form/tests/array.svelte b/packages/svelte-form/tests/array.svelte new file mode 100644 index 000000000..e88cbe0a1 --- /dev/null +++ b/packages/svelte-form/tests/array.svelte @@ -0,0 +1,20 @@ + + + + + + {#snippet children(field)} +
{JSON.stringify(field.state.value)}
+ + {/snippet} +
diff --git a/packages/svelte-form/tests/array.test.ts b/packages/svelte-form/tests/array.test.ts new file mode 100644 index 000000000..852dcd3d5 --- /dev/null +++ b/packages/svelte-form/tests/array.test.ts @@ -0,0 +1,27 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { userEvent } from '@testing-library/user-event' +import { mount, unmount } from 'svelte' +import TestForm from './array.svelte' + +describe('Svelte Field array mode', () => { + let element: HTMLDivElement + let instance: any + beforeEach(async () => { + element = document.createElement('div') + document.body.appendChild(element) + instance = mount(TestForm, { + target: element, + }) + }) + + afterEach(() => { + unmount(instance) + element.remove() + }) + + it('should support array mode', async () => { + expect(element.querySelector('#val')!.textContent).toBe('["a"]') + await userEvent.click(element.querySelector('#push')!) + expect(element.querySelector('#val')!.textContent).toBe('["a","b"]') + }) +})