reactjs - Hur skapar man en återanvändbar react-hook-form-komponent med TypeScript-generics?


Person-komponenten återanvänds i två olika former:

Person.tsx

import { UseFormReturn } from "react-hook-form";
import { FieldPaths } from "./types";

type Props<TFormValues> = {
  methods: UseFormReturn<TFormValues>;
  fieldPaths: FieldPaths<TFormValues>;
};

const Person = <TFormValues,>({
  methods,
  fieldPaths
}: Props<TFormValues>) => {
  const { register } = methods;

  return (
    <div className="person">
      <div className="name-field">
        <input {...register(fieldPaths.firstName)} placeholder="First name" />
        <input {...register(fieldPaths.lastName)} placeholder="Last name" />
      </div>
      <div className="radio-group">
        <label>
          <input type="radio" value="free" {...register(fieldPaths.status)} />{" "}
          Free
        </label>
        <label>
          <input type="radio" value="busy" {...register(fieldPaths.status)} />{" "}
          Busy
        </label>
      </div>
    </div>
  );
};

export { Person };

SimpleForm.tsx

import { useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";

type SimpleFormValues = {
  person: TPerson;
};

const simpleFormFieldPaths = {
  firstName: "person.name.first",
  lastName: "person.name.last",
  status: "person.status"
} as const;

const SimpleForm = () => {
  const methods = useForm<SimpleFormValues>({
    defaultValues: {
      person: emptyPerson
    }
  });
  const { handleSubmit } = methods;
  const onSubmit = ({ person }: SimpleFormValues) => {
    console.log(JSON.stringify(person, null, 2));
  };

  return (
    <div>
      <h2>Simple form</h2>
      <form method="post" onSubmit={handleSubmit(onSubmit)}>
        <Person methods={methods} fieldPaths={simpleFormFieldPaths} />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export { SimpleForm };

ComplexForm.tsx

import { useFieldArray, useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";

type ComplexFormValues = {
  artists: TPerson[];
};

const ComplexForm = () => {
  const methods = useForm<ComplexFormValues>({
    defaultValues: {
      artists: [emptyPerson, emptyPerson, emptyPerson]
    }
  });
  const { control, handleSubmit } = methods;
  const { fields } = useFieldArray({
    control,
    name: "artists"
  });
  const onSubmit = ({ artists }: ComplexFormValues) => {
    console.log(JSON.stringify(artists, null, 2));
  };

  return (
    <div>
      <h2>Complex form</h2>
      <form method="post" onSubmit={handleSubmit(onSubmit)}>
        {fields.map((field, index) => {
          const complexFormFieldPaths = {
            firstName: `artists.${index}.name.first`,
            lastName: `artists.${index}.name.last`,
            status: `artists.${index}.status`
          } as const;

          return (
            <Person
              methods={methods}
              fieldPaths={complexFormFieldPaths}
              key={field.id}
            />
          );
        })}
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export { ComplexForm };

types.ts

import { FieldPath } from "react-hook-form";

export type FieldPaths<TFormValues> = {
  firstName: FieldPath<TFormValues>;
  lastName: FieldPath<TFormValues>;
  status: FieldPath<TFormValues>;
};

export type TPerson = {
  name: {
    first: string;
    last: string;
  };
  status: "" | "free" | "busy";
};

utils.ts

import { TPerson } from "./types";

export const emptyPerson: TPerson = {
  name: {
    first: "",
    last: ""
  },
  status: ""
};

Det fungerar utmärkt, men om jag försöker att setValue i Person, kastar TypeScript ett fel:

const { setValue } = methods;

setValue(fieldPaths.firstName, "David");

Argument av typen 'string' kan inte tilldelas till parametern av typen 'UnpackNestedValue>'

Hur skulle jag kunna fixa detta?

CodeSandbox


Jag tror att det jag vill är att antyda för TypeScript att PathValue(TFormValues, typeof fieldPaths.firstName) alltid kommer att vara en string, och då borde det fungera, men jag är inte säker på hur man uppnår det.

Observera: PathValue definieras här.

Svar #1

Det ser ut som att detta brukade vara fixat i slutet av react-hook-form@6; sedan kom v7 som återinförde problemet och verkar ha förts vidare till v8 beta också.

Angående din fråga

"Ange för TypeScript att PathValue(TFormValues, typeof fieldPaths.firstName) alltid kommer att vara en string"

Detta kommer sannolikt inte att fungera, på grund av vad setValue har definierats att acceptera.

I idealfallet skulle vi ta upp frågan med författarna och hoppas på en snabb lösning, men i avsaknad av det finns det två lösningar här:

  1. Antingen alias UnpackNestedValue och kasta till det

    import { UnpackNestedValue, PathValue, Path } from "react-hook-form";
    
    type ValueArgument<f> = UnpackNestedValue<PathValue<f, Path<f>>>;
    
    setValue(fieldPaths.firstName, "David" as ValueArgument<TFormValues>);
    
  2. Utöka typerna hela vägen upp för att acceptera primitiva typer

    import {
      UseFormReturn,
      FieldValues,
      FieldPath,
      UnpackNestedValue,
      FieldPathValue,
      SetValueConfig,
    } from "react-hook-form";
    import { FieldPaths } from "./types";
    
    type UseMyFormSetValue<TFieldValues extends FieldValues> = <
      TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
    >(
      name: TFieldName,
      value: UnpackNestedValue<FieldPathValue<TFieldValues, TFieldName>> | string,
      options?: SetValueConfig
    ) => void;
    
    interface UseMyFormReturn<TFormValues> extends UseFormReturn<TFormValues> {
      setValue: UseMyFormSetValue<TFormValues>;
    }
    
    type Props<TFormValues> = {
      methods: UseMyFormReturn<TFormValues>;
      fieldPaths: FieldPaths<TFormValues>;
    };
    
    const Person = <TFormValues>({ methods, fieldPaths }: Props<TFormValues>) => {
      const { register, setValue } = methods;
    
      const changeName = () => {
        setValue(fieldPaths.firstName, "David");
      };
    

Relaterade artiklar