[v2][vue] Field components registered with `createFormHook` don't rerender on field state changes, and lose their field API after `form.reset()`

Aberta
#2,390 2 comentários 1 reação 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
3/5
Tempo estimado
1-2 dias
Facilidade para iniciantes
35/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
typescript
Domínio
frontend

Direção de pesquisa

Comece por src/AppForm/fieldComponentHelpers.lib.ts e src/VueForm/Components.lib.ts e, em seguida, inspecione os testes de regressão referenciados em PR #2392. Reproduza a falha com a configuração de testes do formulário Vue e verifique se os componentes de campo injetados são atualizados após alterações na validação e se continuam atualizando o formulário após form.reset().

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

Describe the bug

In @tanstack/[email protected], field components registered through createFormHook with getFormHookHelpers().fieldComponent.strict() (the pattern used in examples/vue/large-form) only render once:

  1. They don't rerender when the field state changes. After a change that produces a validation error, the form state is correct (form.getFieldMeta('name').errors contains the error and meta.isInvalid is true), but the injected component keeps showing its first render: no error message and aria-invalid="false". Rendering the same field directly inside the <form.Field> slot updates as expected.
  2. They lose their field API after form.reset(). After a reset, typing into the injected component no longer updates the form value.

Root cause

  • wrapField (src/AppForm/fieldComponentHelpers.lib.ts) injects the field API and passes the same object as a prop on every render. The parent Field rerenders its slot because of its own subscription, but Vue skips updating the injected component because its props are unchanged (same object reference), and the child's reads of the field API getters are not tracked by Vue.
  • createFieldComponent (src/VueForm/Components.lib.ts) calls provide(fieldContext, fieldApi.value) once during setup. useField replaces fieldApi.value when resetVersion changes, so after form.reset() the injected component keeps the old instance. The comment above that line ("Field APIs are stable for a mounted name… the parent subscription handles state-driven renders") doesn't hold for injected components.
Your minimal, reproducible example

Regression tests are included in PR #2392. Minimal Vue SFC repro:

<!-- TextField.vue — same as examples/vue/large-form/src/components/TextField.vue -->
<script setup lang="ts">
import type { FieldWithValue } from '@tanstack/vue-form'
defineProps<{ field: FieldWithValue<string>; label: string }>()
</script>

<template>
  <label :for="field.name">{{ label }}</label>
  <input
    :id="field.name"
    :value="field.value"
    :aria-invalid="field.meta.isInvalid"
    @input="field.handleChange(($event.target as HTMLInputElement).value)"
  />
  <p v-for="error in field.errors" :key="error.message">{{ error.message }}</p>
</template>
<!-- Form.vue -->
<script setup lang="ts">
import { createFormHook, getFormHookHelpers } from '@tanstack/vue-form'
import TextField from './TextField.vue'

const { fieldComponent } = getFormHookHelpers()
const { useAppForm } = createFormHook({
  fieldComponents: { TextField: fieldComponent.strict(TextField, 'field') },
  formComponents: {},
})

const form = useAppForm({
  defaultValues: { name: '' },
  validators: [
    {
      triggers: ['change'],
      run: ({ value, createErrorMap }) => {
        const errors = createErrorMap()
        if (value.name.length < 2) errors.fields.name = 'At least 2 characters'
        return errors
      },
    },
  ],
})
</script>

<template>
  <form.Field name="name" v-slot="{ field }">
    <field.TextField label="Name" />
  </form.Field>
  <button type="button" @click="form.reset()">Reset</button>
</template>
Steps to reproduce
  1. Render Form.vue.
  2. Type one character into the input.
  3. form.getFieldMeta('name').errors contains "At least 2 characters", but TextField shows no error and aria-invalid stays "false".
  4. Click Reset, then type again: form.state.values.name doesn't change.
Expected behavior

Injected field components rerender when their field state changes and keep working after form.reset(), the same as markup rendered directly inside the <form.Field> slot.

How often does this bug happen?

Every time

Platform
  • macOS, Node 24
  • Reproduced in Vitest with both happy-dom and jsdom (the upstream packages/vue-form test setup)
  • Vue 3.6.0-rc.8
TanStack Form adapter

vue-form

TanStack Form version

2.0.0-alpha.2 (also reproduced on the alpha branch at 6e5b809)

TypeScript version

6.0.3

Additional context

I opened PR #2392 with a fix and two regression tests. Both tests fail before the change and pass after it.

Linguagem predominante
TypeScript
Estrelas
6.8k
Forks
705
Merge médio
1d 7h
PRs com merge (30d)
13

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de TanStack/form

Todas as issues de TanStack/form

Issues semelhantes

Mais issues de TypeScript

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.