Skip to content

feat(web): додати useApiForm — уніфікований form-engine на RHF + zod#1614

Merged
Skords-01 merged 1 commit into
mainfrom
devin/1777886254-useapiform
May 4, 2026
Merged

feat(web): додати useApiForm — уніфікований form-engine на RHF + zod#1614
Skords-01 merged 1 commit into
mainfrom
devin/1777886254-useapiform

Conversation

@Skords-01
Copy link
Copy Markdown
Owner

@Skords-01 Skords-01 commented May 4, 2026

Summary

Закриває item #8 з docs/diagnostics/2026-05-03-web-deep-dive/00-overview.md (Score 1.67, §3.1 frontend-ergonomics).

До цього у репо ~30+ форм використовували розрізнені підходи: useFormValidation custom hook, inline validate() функції, ad-hoc submit handlers — без shared error UX, з дублюванням логіки "disable submit if pristine", без уніфікованої server-error mapping.

Цей PR створює foundation: новий useApiForm hook на стандарт-стеку react-hook-form + @hookform/resolvers/zod. Існуючі форми мігруватимуться поступово в окремих follow-up PR-ах (auth, finyk transactions, fizruk template, nutrition food add, routine task create — найвищий traffic спочатку).

Що додано

apps/web/src/shared/forms/useApiForm.ts — wrapper, який обʼєднує:

  • react-hook-form як state engine (register, handleSubmit, formState).
  • zod через @hookform/resolvers/zod — client-side валідація з тих самих схем, які можна шерити з сервером.
  • Server-error mappingApiError зі структурою { error, details: [{ path, message }] } (саме той формат, який віддає apps/server/src/http/validate.ts) автоматично перетворюється на setError(path, …) для кожного поля. Top-level помилки → serverError для banner-у над формою.
  • submit — pre-bound handleSubmit(handle) для <form onSubmit={submit}> шаблону.
  • isSubmitting — гарантовано clears навіть якщо handler не кинув виключення.
  • lastResponse: TResponse | undefined — типізована відповідь після успіху.
  • resetOnSuccess?: boolean — опція ресету форми.
  • clearServerError() — ручне скидання banner-помилки (на focus наступного поля).

apps/web/src/shared/forms/useApiForm.test.tsx — 12 тестів:

# Тест
1 happy path: валідні дані → onSubmit
2 client-side: zod помилки блокують submit
3 server: details[].path → setError на поля
4 server: 500 без details → top-level serverError
5 server: details із пустим path → top-level error
6 non-ApiError помилка (мережева): передається як-є
7 isSubmitting: кнопка disabled під час pending
8 submit disabled поки форма pristine
9 submit enabled після першої взаємодії (dirty)
10 resetOnSuccess: значення скидаються після успіху
11 clearServerError: top-level error можна скинути
12 повторний submit після server-error скидає попередні помилки полів

apps/web/src/shared/hooks/useFormValidation.ts — помічено @deprecated. Існуючі споживачі (ManualExpenseSheet, ResetPasswordPage) лишаються до окремих migration-PR-ів.

apps/web/package.json — додано react-hook-form ^7.75.0 та @hookform/resolvers ^5.2.2.

Що НЕ покрито у цьому PR

  • Міграція існуючих форм. Це навмисно окремі PR-и для безпечного rollout (кожна форма — окрема одиниця ризику з власними edge-cases).
  • ESLint custom rule prefer-use-api-form через eslint-plugin-sergeant-design — diagnostic згадує опціонально; раціональніше додати після першої хвилі міграцій, коли стає зрозуміло, які форми мають винятки.

Governing Skill

  • Primary skill: sergeant-feature-delivery
  • Secondary skill: sergeant-web-ui (бо це shared UI hook)

Playbook

  • Primary playbook: n/a — нема specialized playbook для introduction нового shared hook
  • If no playbook matched, why: ці два спеціалісти-skill вистачають

Verification

$ cd apps/web && npx vitest run src/shared/forms/useApiForm.test.tsx
 ✓ src/shared/forms/useApiForm.test.tsx  (12 tests) 230ms
 Test Files  1 passed (1)
      Tests  12 passed (12)

$ npx eslint src/shared/forms/ src/shared/hooks/useFormValidation.ts
# clean

$ npx tsc --noEmit -p tsconfig.json | grep useApiForm
# no errors specific to my file

Additional checks:

  • Local smoke / manual validation completed
  • Surface-specific checks completed

Docs and Governance

  • I updated docs that changed with the behavior, contract, workflow, or rollout.
  • I checked whether AGENTS.md needed an update.
  • I checked whether a playbook or skill needed an update.
  • I checked whether governance docs or review docs needed an update.

Updated docs:

Risk and Rollout

  • User-visible risk: none — additive-only, нічого старого не видаляє і не міняє runtime-поведінку. useFormValidation тільки помічений @deprecated, всі споживачі продовжують працювати.
  • Rollout / deploy order: standard. Bundle сайз: react-hook-form ~12kb (gzipped), @hookform/resolvers/zod ~1kb — зайдуть тільки коли перша форма мігрує. Tree-shake поки що ефективно випиляє hook.
  • Backout plan: revert single commit; видаляє новий hook + dev-deps + знімає @deprecated-marker.

Hard Rule #15

  • I read AGENTS.md before coding.
  • Internal docs I touched are in Ukrainian.
  • I did not use --no-verify. Pre-commit hook (lint-staged + ESLint + Prettier) пройшов чисто.

Reviewer Notes

  • Чому RHF + zod, а не Formik / react-final-form? RHF — найменший bundle (~12kb gzipped vs Formik ~16kb), найкраща performance (uncontrolled inputs by default), нативний integration з zod через @hookform/resolvers. zod вже в репо як основна schema-library і шериться з сервером.
  • Server-error contract. Hook покладається на стабільний контракт apps/server/src/http/validate.ts: 400 → { error: string, details: [{ path: string, message: string }] }. Якщо сервер змінить shape, треба оновити функцію applyServerError у useApiForm.ts. Контракт уже використовується скрізь (~30 endpoints).
  • details[].path як dot-path. Server віддає path: "user.email" для nested fields — RHF setError теж приймає dot-path через Path<TValues> тип. Сумісно out-of-the-box.
  • Чому isSubmitting має внутрішній прапорець? RHF formState.isSubmitting сам слідкує за promise з handleSubmit, але якщо handler-помилка ловиться у applyServerError, RHF тимчасово думає що submit ще активний. Внутрішній internalSubmitting гарантовано clears у finally.
  • Чому окремий PR і чому не міграція? Diagnostic явно рекомендує incremental rollout. Спочатку foundation (цей PR), потім — окремі migration PR-и по топ-формам (auth перша, finyk транзакції друга, бо найбільший traffic). Кожна форма має свої edge-cases (custom validation, multi-step, optimistic updates) — не варто змішувати у one big bang.
  • Pre-existing CI failures: Test coverage, check (governance-sync), Critical-flow E2E, markdown link checker — pre-existing на main; цей PR не додає нових.

Summary by cubic

Introduce useApiForm: a unified form engine for web built on react-hook-form and zod resolver. It standardizes validation, server error mapping, and submit UX across forms. Closes diagnostics item #8 (§3.1 frontend-ergonomics).

  • New Features

    • Added useApiForm (shared/forms/useApiForm.ts) using react-hook-form + @hookform/resolvers (zod).
    • Maps ApiError { error, details: [{ path, message }] } to field errors and top-level serverError.
    • Provides submit, isSubmitting, lastResponse, resetOnSuccess, clearServerError.
    • Barrel export at shared/forms/index.ts.
    • Marked useFormValidation as @deprecated (existing consumers keep working).
    • Added 12 tests covering happy path, zod validation, server mapping, pristine/dirty, reset, retry.
  • Dependencies

    • Added react-hook-form ^7.75.0 and @hookform/resolvers ^5.2.2.

Written for commit 034cc0f. Summary will update on new commits.

Закриває §3.1 з docs/diagnostics/2026-05-03-web-deep-dive — роадмап
item #8. До цього у репо ~30+ форм використовували розрізнені
підходи (useFormValidation hook, inline validate, ad-hoc submit
handlers) → немає shared error UX, дублювання логіки 'disable submit
if pristine', відсутня уніфікована server-error mapping.

apps/web/src/shared/forms/useApiForm.ts:
  - Wrapper над react-hook-form + @hookform/resolvers/zod
  - Автоматичне мапування серверних 400-помилок з shape
    { error, details: [{ path, message }] } (як вертає
    apps/server/src/http/validate.ts) → setError(path, ...) на полях.
    Top-level помилки (без path) → window.serverError.
  - submit(): pre-bound handleSubmit для <form onSubmit>
  - isSubmitting: гарантовано клириться навіть якщо handler не кинув
  - lastResponse: typed response після успіху
  - resetOnSuccess: опція ресету форми
  - clearServerError: ручне скидання banner-помилки

apps/web/src/shared/forms/useApiForm.test.tsx:
  - 12 тестів: happy path, zod валідація, server details mapping,
    top-level error, isSubmitting state, dirty-state, resetOnSuccess,
    clearServerError, повторний submit після помилки.

apps/web/src/shared/hooks/useFormValidation.ts:
  - Помічено @deprecated. Існуючі споживачі (ManualExpenseSheet,
    ResetPasswordPage) лишаються — мігруються в окремих PR-ах.

apps/web/package.json: + react-hook-form ^7.75, @hookform/resolvers ^5.2.

Refs:
- docs/diagnostics/2026-05-03-web-deep-dive/01-frontend-ergonomics.md §3.1
- apps/server/src/http/validate.ts (server details shape)
- packages/api-client/src/ApiError.ts (ApiError.body)
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sergeant Ready Ready Preview, Comment May 4, 2026 9:32am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@Skords-01 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 42 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7527e642-f132-422a-8c6f-d24130e826aa

📥 Commits

Reviewing files that changed from the base of the PR and between bf291e9 and 034cc0f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • apps/web/package.json
  • apps/web/src/shared/forms/index.ts
  • apps/web/src/shared/forms/useApiForm.test.tsx
  • apps/web/src/shared/forms/useApiForm.ts
  • apps/web/src/shared/hooks/useFormValidation.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1777886254-useapiform

Review rate limit: 0/10 reviews remaining, refill in 2 minutes and 42 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/L label May 4, 2026
@Skords-01 Skords-01 merged commit e1e1cdd into main May 4, 2026
28 of 47 checks passed
@Skords-01 Skords-01 deleted the devin/1777886254-useapiform branch May 4, 2026 09:38
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

⏱️ CI Pipeline Duration Report

Based on the last 50 successful runs on the default branch.

Overall Pipeline

Metric Value
p50 6m 26s
p95 7m 55s
p99 9m 3s
Current run 6m 25s
vs p95 -18.9%

Trend (last 20 runs): ▃▃▁▂▃▃▃▂▃▃▂▂▄▃▃▆▅▄█▆

Per-Job Breakdown

Job p50 p95 p99 Current vs p95
Accessibility (axe-core) 2m 5s 2m 21s 2m 23s 0s -100.0%
Commit messages (commitlint) 0s 0s 0s 34s N/A
Critical-flow E2E (Playwright) 1m 36s 1m 44s 1m 44s 5m 59s +245.2%
Migration lint (AGENTS rule 0s 0s 0s 9s N/A
Pipeline duration (p95 trend) 26s 27s 27s
Secret scan (gitleaks) 8s 11s 11s 12s +9.1%
Smoke E2E (Playwright) 1m 26s 1m 40s 1m 40s
Test coverage (vitest) 2m 4s 2m 33s 2m 33s 2m 3s -19.6%
Workflow lint (actionlint) 7s 7s 7s 7s +0.0%
check 4m 12s 4m 54s 5m 6s 47s -84.0%
tsconfig strict guard (PR-1.A) 5s 14s 14s 7s -50.0%

Skords-01 added a commit that referenced this pull request May 4, 2026
Foundation для useApiForm зайшла у #1614 (hook + 12 тестів). Цей
PR — перший high-traffic call-site з roadmap-у. Закриває Item 8
follow-up з docs/diagnostics/2026-05-03-web-deep-dive.

Зміни в AuthPage:

* Виокремив дві суб-форми <LoginForm /> та <RegisterForm />, кожна
  на своєму useApiForm з власною zod-схемою (loginSchema vs
  registerSchema). Це надійніше за умовну схему всередині однієї
  форми — RHF фіксує zodResolver на mount.
* loginSchema перевіряє email-формат і вимагає не-порожній пароль
  (мінімальна довжина — на сервері; legacy-акаунти мають короткі
  паролі).
* registerSchema enforces 10-character minimum, до 128 max, optional
  name (fallback на email-prefix у onSubmit).
* PasswordVisibilityToggle і FieldError виокремлені в локальні
  компоненти — щоб не дублювати JSX між login та register.

useApiForm дає:
* isSubmitting (замість руками тримати loading useState)
* defaultValues + register() (замість chain useState + onChange)
* formState.errors (per-field client-side помилки)

Серверні помилки далі йдуть через AuthContext.authError — не
інтегрував з form.serverError, бо authError уже володіє
локалізацією Better Auth (translateAuthError) і використовується
паралельно у forgot-password і Google flows. Кидаємо мовчазний
new Error("") у onSubmit при ok=false щоб придушити onSuccess
(toast / achievement) — текст помилки рендериться через authError.

Тести (apps/web/src/core/auth/AuthPage.test.tsx, 8 тестів):
* zod-валідація порожніх / невалідних полів
* happy path login → toast 'Вхід виконано'
* login fail → no toast, authError рендериться як alert
* register: 10-char minimum
* register: name fallback на email-prefix
* register: явне ім'я використовується as-is
* mode switch скидає authError

Co-Authored-By: Андрій Виграв <dmytro.s.stakhov@gmail.com>
Skords-01 added a commit that referenced this pull request May 4, 2026
…pline

Closes adoption gap left by 0007/0008/0009.

Foundation-tools without consumers: #1614 useApiForm,

Process-incidents to close: #1571 empty-body,

Plus CSP_DISABLE retrospective audit.

Co-Authored-By: Сон Хер <dmytro.s.stakhov@gmail.com>
Skords-01 added a commit that referenced this pull request May 4, 2026
…pline

Closes adoption gap left by 0007/0008/0009.

Foundation-tools without consumers: #1614 useApiForm,

Process-incidents to close: #1571 empty-body,

Plus CSP_DISABLE retrospective audit.

Co-Authored-By: Сон Хер <dmytro.s.stakhov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant