Pesquisa de site

reactjs - Problema com tipo genérico em um componente react-hook-form


Estou tentando construir um componente de array de campos em TypeScript que possa funcionar com mais de um formulário. Para fazer isso, estou tentando usar um genérico para passar a interface dos campos do formulário, mas não consigo acertar os tipos (está funcionando, mas tem problemas de tipo).

Se eu importar minha interface (que descreve os campos do meu formulário) e usá-la no lugar de T, tudo funciona como esperado, mas com o genérico eu recebo alguns erros. Aqui está o que eu tenho:

import type { Control, FieldErrors, FieldValues } from 'react-hook-form'
import { useFieldArray } from 'react-hook-form'

interface Props<T extends FieldValues> {
  control: Control<T>
  errors: FieldErrors<T>
}

export function Tags<T extends FieldValues>({
  control,
  errors,
}: Props<T>) {
  const { fields, append, remove } = useFieldArray({
    name: 'tags',
    control,
    keyName: 'key',
    rules: { required: 'Please choose at least one tag' },
  })

  const handleTagChange = (tag: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value, checked } = tag.target

    if (checked) {
      append({ id: value, name })
    } else {
      const index = fields.findIndex((field) => field.id === value)
      remove(index)
    }
  }

  const tags = [
    { id: '1', name: 'tag1' },
    { id: '2', name: 'tag2' },
    { id: '3', name: 'tag3' },
  ]

  return (
    <>
      <div role="group" aria-labelledby="policy-tags">
        <p id="policy-tags">Tags</p>
        <div>
          {tags.map((tag) => (
            <>
              <label key={tag.id} htmlFor={tag.name}>
                {tag.name}
              </label>
              <input
                type="checkbox"
                onChange={(e) => handleTagChange(e)}
                key={tag.id}
                name={tag.name}
                value={tag.id}
                checked={fields.some((field) => field.id === tag.id)}
              />
            </>
          ))}
        </div>
        {errors?.tags?.root && <p>{errors.tags.root?.message || ''}</p>}
      </div>
    </>
  )
}

E a implementação:

<Tags<IFormFields> control={control} errors={errors} />

Estou vendo esses erros:

O componente funciona como esperado, só que não consigo acertar os tipos. Alguma ideia?

Resposta #1

Você está muito perto! As seguintes alterações são necessárias para fazer o TypeScript entender como os dados estão fluindo:

  • useFieldArray pode receber 3 argumentos de tipo; TFieldValues, TFieldArrayName e TKeyName (veja a documentação do ts)
    Como useFieldArray participa como um campo no seu formulário, ele requer que o nome (TFieldArrayName) seja parte de TFieldValues. useFieldArray tenta inferir os campos do formulário através do tipo control, mas isso acaba - via seu Props - em FieldValues vazio, onde 'tags' não existe.
    Então, como @Moussa afirmou, você precisa fornecer um formulário parcial com apenas o campo de tag necessário.

  • O tipo da propriedade control deve ser sobrescrito devido à passagem genérica interna complexa (causada pelas propriedades _subjects.state e _reset, que não podem corresponder T a FieldValues embora T estenda FieldValues). Ao definir TFieldValues como Partial<T>, o tipo control pode ser sobrescrito com segurança usando Control<FieldValues>

Solução

Com as alterações acima, o seguinte trecho de código deve funcionar:

interface PartialTagForm {
  tags: { id: string; name: string }[]
}

export function Tags<T extends FieldValues>({ control, errors }: Props<T>) {
  const { fields, append, remove } = useFieldArray<
    Partial<PartialTagForm>,
    'tags',
    'key'
  >({
    name: 'tags',
    control: control as Control<FieldValues>,
    keyName: 'key',
    rules: { required: 'Please choose at least one tag' },
  })

Bônus

As tags devem sempre estar disponíveis como chave no seu formulário, você deve substituir FieldValues por PartialTagForm em Tags<T extends FieldValues>. Note que tags então também deve estar disponível no seu IFormFields

Resposta #2

Para o primeiro problema, você deve definir o formato do seu tipo e fornecê-lo ao hook useFieldArray da seguinte forma:

interface FormValues {
  tags: { id: string; name: string }[];
}

const { fields, append, remove } = useFieldArray<FormValues>({
  name: 'tags' as const, // 'as const' asserts 'tags' as a literal type
  control,
});

Fazer isso também deve resolver o segundo problema. Por favor, tente o que foi sugerido acima e poste uma resposta com sua atualização. Vou tentar editar a resposta de acordo.