Skip to content

feat(#3): Formik adapter — formdraft/formik subpath#10

Merged
mayrang merged 1 commit into
mainfrom
feat/issue-3-formik-adapter
May 27, 2026
Merged

feat(#3): Formik adapter — formdraft/formik subpath#10
mayrang merged 1 commit into
mainfrom
feat/issue-3-formik-adapter

Conversation

@mayrang
Copy link
Copy Markdown
Owner

@mayrang mayrang commented May 27, 2026

Closes #3.

Summary

  • New subpath formdraft/formik exports useFormDraftFormik(formik, options)
  • Mirrors the formdraft/rhf pattern: thin adapter over useFormDraft, returns the persistence-layer surface (status, lastSavedAt, error, save, discard, etc.) without the value/set/patch API (Formik owns those)
  • formik added as optional peer dependency

Vetting

5 rounds of code review + adversarial audit dispatched in parallel each round. Real bugs caught:

  • discard didn't reset the visible Formik form → wrapped to call form.resetForm()
  • restore triggered validation, painting errors on un-typed restored data → setValues(value, false)
  • discard identity churned when consumers passed inline Formik handlers → formRef pattern
  • restore caused a sync round-trip on every page load → ignoreNextFormChangeRef gate
  • delete-back-to-initial silently kept stored draft alive (canonical autosave failure) → sticky userTouchedRef
  • type-then-clear-then-late-restore could resurrect cleared input (latent race) → covered by useFormDraft's own userTouchedRef + adapter latching hasRestoredRef on dirty-skip

Test plan

  • npm test — 115 unit tests pass (10 Formik adapter + 105 existing)
  • npx size-limit — 4.09 KB brotli (Formik adapter in its own chunk, main entry unchanged)
  • Manual: integrate the README example into a Formik+Zod profile-form, verify refresh restores draft + discard clears + submit-pattern clears

Closes #3. Mirrors the formdraft/rhf pattern for users on Formik.

  const formik = useFormik({
    initialValues: { name: '', bio: '' },
    onSubmit: async (v) => {
      await api.submit(v);
      discard(); // clears storage + broadcasts + resets formik
    },
  });
  const { status, lastSavedAt, discard } = useFormDraftFormik(formik, {
    key: 'profile-form',
    schema: zodAdapter(Schema),
    sync: api.saveProfile,
  });

## Vetting

5 rounds of code review + adversarial audit dispatched in parallel each round.
Real bugs caught and fixed before merge:

  R1: discard wiped storage but left the visible formik input intact, so
      the next keystroke re-persisted the "discarded" text back to storage.
      → discard now also calls form.resetForm().
  R1: restore called form.setValues(value) which defaulted shouldValidate=true,
      painting validation errors against text the user never typed.
      → setValues(draft.values, false).
  R2: identity churn — Formik recreates handlers on every render with inline
      onReset/initialErrors, making `discard` re-create each render and break
      consumer useEffect deps.
      → formRef pattern reads latest form.resetForm via ref.
  R3: restore caused a sync round-trip on every page load (setValues leaves
      formik.dirty=true → persist effect fires → patches restored data back
      into draft → enqueues sync of unchanged values).
      → ignoreNextFormChangeRef set by restore-effect, consumed + cleared
        by the next value-watcher run.
  R3: delete-back-to-initial silently dropped (formik.dirty flips false →
      patch effect skipped → stored draft survives → next mount restores
      "deleted" text). Canonical autosave failure mode.
      → sticky userTouchedRef (set on observed dirty=true, never auto-reset);
        discard() resets it so post-discard resetForm doesn't re-patch.
  R4: latch hasRestoredRef when restore skipped due to form.dirty so we
      don't re-evaluate it on every subsequent patch.

## Tests

10 unit tests:
  - persist on input change
  - restore from storage on mount
  - restore does NOT trigger validation (F1 regression)
  - discard clears storage AND visible input (F3 regression)
  - submit pattern: discard in onSubmit clears storage
  - delete-back-to-initial persists deletion (F2 regression)
  - restore does NOT trigger redundant sync (F1/ignoreNext regression)
  - type-clear-late-restore: cleared input stays cleared (D5 latent race)
  - user-types-before-restore race (deferred storage harness)
  - underlying useFormDraft cleanup verified via discard

Wired in:
  - package.json adds formdraft/formik to exports + formik as optional peer
  - tsup adds formik/index entry → dist/formik/{index.mjs,index.js,index.d.ts}
  - README adds Formik integration section with submit-pattern example

Total 115 unit tests pass. Bundle 4.09 KB brotli (Formik adapter goes into
its own chunk; main entry size unchanged).
@mayrang mayrang merged commit 00d5f8f into main May 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.2: Formik adapter (formdraft/formik)

1 participant