Skip to content

Пример: редактор поста

Форма поста показывает границу между React Hook Form, Valibot и MobX.

Ответственность

React Hook Form владеет:

  • текущими значениями полей;
  • touched/dirty;
  • ошибками полей;
  • submit lifecycle формы.

MobX store владеет состоянием сценария, которое не является состоянием конкретного input.

Например:

  • сохраняется ли пост;
  • какой post был сохранён;
  • общая ошибка сохранения.

Schema формы

ts
// data/post-editor/post-form.schema.ts
import {
  minLength,
  object,
  pipe,
  string,
  type InferOutput,
} from "valibot"

export const PostFormSchema = object({
  title: pipe(string(), minLength(1, "Введите заголовок")),
  body: pipe(string(), minLength(1, "Введите текст")),
})

export type PostFormValues = InferOutput<typeof PostFormSchema>

Store сценария

ts
// data/post-editor/post-editor.store.ts
import { makeAutoObservable } from "mobx"

export class PostEditorStore {
  isSaving = false
  savedPostId: string | null = null

  constructor() {
    makeAutoObservable(this)
  }

  startSaving(): void {
    this.isSaving = true
  }

  finishSaving(postId: string): void {
    this.isSaving = false
    this.savedPostId = postId
  }

  reset(): void {
    this.isSaving = false
    this.savedPostId = null
  }
}

В этом store нет копии title и body: ими уже владеет React Hook Form.

React Hook Form + Valibot

tsx
import { valibotResolver } from "@hookform/resolvers/valibot"
import { useForm } from "react-hook-form"

import {
  PostFormSchema,
  type PostFormValues,
} from "@/data/post-editor"

export const PostEditorForm = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<PostFormValues>({
    resolver: valibotResolver(PostFormSchema),
    defaultValues: {
      title: "",
      body: "",
    },
  })

  const titleField = register("title")
  const bodyField = register("body")

  const submit = (values: PostFormValues) => {
    // Передайте проверенные значения владельцу сценария сохранения.
  }

  return (
    <form onSubmit={handleSubmit(submit)}>
      <label>
        Заголовок
        <input
          name={titleField.name}
          onBlur={titleField.onBlur}
          onChange={titleField.onChange}
          ref={titleField.ref}
        />
      </label>
      {errors.title && <p>{errors.title.message}</p>}

      <label>
        Текст
        <textarea
          name={bodyField.name}
          onBlur={bodyField.onBlur}
          onChange={bodyField.onChange}
          ref={bodyField.ref}
        />
      </label>
      {errors.body && <p>{errors.body.message}</p>}

      <button type="submit">Сохранить</button>
    </form>
  )
}

PostEditorForm.displayName = "PostEditorForm"

Когда нужен Controller

Используйте Controller, если UI-компонент не работает как обычный uncontrolled input и требует явного value/onChange.

Для обычного <input> предпочтительнее register.

Когда нужен FormProvider

Если форма разбита на глубокие внутренние секции, можно передать methods через FormProvider, а в дочерних блоках использовать useFormContext.

Не используйте provider для маленькой формы только ради единообразия.

Главное правило

Не создавайте два источника истины для одних полей.

txt
поля формы → React Hook Form
сценарное состояние → MobX
проверка формы → Valibot