【Tanstack Form】useFormの戻り型を定義する【React】

こんにちは、フリーランスエンジニアの太田雅昭です。

useFormの戻りが複雑

@tanstack/react-formは v1.23.4時点で、useFormの戻りを生成するユーティリティはありません。ジェネリクスも複雑で、自前で生成するのも困難な代物となっています。

export declare function useForm<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(opts?: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>): ReactFormExtendedApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>;

ためしにジェネリクスの第一引数にデータ型を渡してあとはanyで埋めるといったことも試しましたが、どうも思わぬところで型エラーになったりします。

ReturnTypeを使う

決して美しくはありませんが、最も確実なのはReturnTypeを使う方法です。

export type XxxForm = ReturnType<typeof useXxx>;

export function useXxx(){
  return useForm(...
}

これを使えば、例えばPropsで受け取る場合

import { useForm, createFormHook, createFormHookContexts } from '@tanstack/react-form'

type AuthForm = ReturnType<typeof useAuthForm>

function useAuthForm() {
  return useForm({
    defaultValues: { email: '', password: '' },
  })
}

export function PropsSample() {
  const form = useAuthForm()
  return (
    <div>
      <EmailFieldByProps form={form} />
      <PasswordFieldByProps form={form} />
    </div>
  )
}

function EmailFieldByProps({ form }: { form: AuthForm }) {
  return (
    <form.Field name="email">
      {(field) => (
        <input
          value={field.state.value ?? ''}
          onChange={(e) => field.handleChange(e.target.value)}
          type="email"
          placeholder="email"
        />
      )}
    </form.Field>
  )
}

function PasswordFieldByProps({ form }: { form: AuthForm }) {
  return (
    <form.Field name="password">
      {(field) => (
        <input
          value={field.state.value ?? ''}
          onChange={(e) => field.handleChange(e.target.value)}
          type="password"
          placeholder="password"
        />
      )}
    </form.Field>
  )
}

contextも使えます。


const AuthFormContext = createContext<AuthForm | null>(null)

function useAuthFormContext() {
  const ctx = useContext(AuthFormContext)
  if (!ctx) throw new Error('AuthFormContext is not provided')
  return ctx
}

export function ContextSample() {
  const form = useAuthForm()
  return (
    <AuthFormContext.Provider value={form}>
      <EmailFieldByContext />
      <PasswordFieldByContext />
    </AuthFormContext.Provider>
  )
}

function EmailFieldByContext() {
  const form = useAuthFormContext()
  return (
    <form.Field name="email">
      {(field) => (
        <input
          value={field.state.value ?? ''}
          onChange={(e) => field.handleChange(e.target.value)}
          type="email"
          placeholder="email"
        />
      )}
    </form.Field>
  )
}

function PasswordFieldByContext() {
  const form = useAuthFormContext()
  return (
    <form.Field name="password">
      {(field) => (
        <input
          value={field.state.value ?? ''}
          onChange={(e) => field.handleChange(e.target.value)}
          type="password"
          placeholder="password"
        />
      )}
    </form.Field>
  )
}