reactjs - Hvordan lage en gjenbrukbar react-hook-form-komponent med TypeScript-generics?


Person-komponenten gjenbrukes i to forskjellige 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 fungerer vakkert, men hvis jeg prøver å setValue i Person, kaster TypeScript en feil:

const { setValue } = methods;

setValue(fieldPaths.firstName, "David");

Argument av typen 'string' kan ikke tildeles parameter av typen 'UnpackNestedValue>'

Hvordan kunne jeg fikse dette?

CodeSandbox


Jeg tror det jeg vil, er å antyde til TypeScript at PathValue(TFormValues, typeof fieldPaths.firstName) alltid kommer til å være en string, og da bør det fungere, men jeg er ikke sikker på hvordan jeg skal oppnå det.

Merk: PathValue er definert her.

Svar #1

Ser ut som dette pleide å være fikset i slutten av react-hook-form@6; deretter kom v7 som gjeninnførte problemet og ser ut til å ha blitt videreført til v8 beta også.

Når det gjelder spørsmålet ditt

"gi TypeScript et hint om at PathValue(TFormValues, typeof fieldPaths.firstName) alltid kommer til å være en string"

Dette vil sannsynligvis ikke fungere, på grunn av hva setValue er definert til å akseptere.

Ideelt sett ville vi presset spørsmålet på forfatterne og håpet på en rask løsning, men i fravær av det, finnes det to løsninger her:

  1. Enten alias UnpackNestedValue og kast til 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. Utvid typene hele veien opp til å akseptere primitive 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");
      };
    

Relaterte artikler