react-f0rm 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +527 -33
- package/dist/devtools/index.cjs.js +737 -0
- package/dist/devtools/index.cjs.js.map +1 -0
- package/dist/devtools/index.d.ts +33 -0
- package/dist/devtools/index.mjs +717 -0
- package/dist/devtools/index.mjs.map +1 -0
- package/dist/form-61297bc0.d.ts +578 -0
- package/dist/form-94c70b4b.mjs +378 -0
- package/dist/form-94c70b4b.mjs.map +1 -0
- package/dist/form-b9441d8c.cjs.js +387 -0
- package/dist/form-b9441d8c.cjs.js.map +1 -0
- package/dist/index.cjs.js +1257 -157
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +357 -53
- package/dist/index.mjs +1676 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +1257 -157
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +2 -2
- package/dist/index.umd.min.js.map +1 -1
- package/dist/resolvers/standard-schema.cjs.js +90 -0
- package/dist/resolvers/standard-schema.cjs.js.map +1 -0
- package/dist/resolvers/standard-schema.d.ts +66 -0
- package/dist/resolvers/standard-schema.mjs +86 -0
- package/dist/resolvers/standard-schema.mjs.map +1 -0
- package/dist/resolvers/yup.cjs.js +12 -2
- package/dist/resolvers/yup.cjs.js.map +1 -1
- package/dist/resolvers/yup.d.ts +2 -2
- package/dist/resolvers/yup.mjs +23 -0
- package/dist/resolvers/yup.mjs.map +1 -0
- package/dist/resolvers/zod.cjs.js +14 -1
- package/dist/resolvers/zod.cjs.js.map +1 -1
- package/dist/resolvers/zod.d.ts +2 -2
- package/dist/resolvers/zod.mjs +23 -0
- package/dist/resolvers/zod.mjs.map +1 -0
- package/dist/validate-148fe167.d.ts +22 -0
- package/package.json +34 -8
- package/dist/form-d06e6444.d.ts +0 -201
- package/dist/index.esm.js +0 -593
- package/dist/index.esm.js.map +0 -1
- package/dist/resolvers/yup.esm.js +0 -13
- package/dist/resolvers/yup.esm.js.map +0 -1
- package/dist/resolvers/zod.esm.js +0 -10
- package/dist/resolvers/zod.esm.js.map +0 -1
- package/dist/validate-0f17f86a.d.ts +0 -8
package/README.md
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
# react-f0rm
|
|
2
2
|
|
|
3
|
+
[](https://github.com/wmzy/react-f0rm/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/react-f0rm)
|
|
5
|
+
[](https://bundlephobia.com/package/react-f0rm)
|
|
6
|
+
[](https://opensource.org/licenses/ISC)
|
|
7
|
+
|
|
8
|
+
A headless, event-driven React form library with field-level subscriptions.
|
|
9
|
+
|
|
3
10
|
## Features
|
|
4
11
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
12
|
+
- **Field-level subscriptions.** Editing one field re-renders exactly that field's component, not the whole form. State is read through `useSyncExternalStore`, so snapshots stay consistent under concurrent rendering (no tearing).
|
|
13
|
+
- **Truly type-safe paths.** `FieldPath<T>` enumerates every valid field name for your values shape and `PathValue<T, P>` resolves the value type at that path — typos in field names fail at compile time, values are inferred.
|
|
14
|
+
- **One schema adapter for every library.** The Standard Schema resolver covers zod (v3.24+/v4), valibot v1, arktype and any other Standard Schema v1 implementation through a single tree-shakeable entry point.
|
|
15
|
+
- **Headless, with accessibility hooks.** You own the markup. When you opt into error rendering via `renderError`, `aria-invalid` and `aria-describedby` are wired up automatically.
|
|
16
|
+
- **Tombstone unregister.** Unmounted fields drop out of `getValues()` instead of silently reviving their initial values on the next read.
|
|
17
|
+
- **Copy-on-write `getValues()`.** An ownership-tracked merge allocates each container once per read instead of re-copying whole branches for every key.
|
|
18
|
+
- **Multiple errors per field.** Each field stores an ordered `FieldError[]` — `getFieldErrors`/`useFieldErrors` read them all, and schema resolvers forward every issue instead of stopping at the first.
|
|
19
|
+
- **Async validation with cancellation.** `validateDebounce` per field plus an `AbortSignal` handed to every validator: a superseded round aborts its in-flight fetch, and pending debounce windows count as validating so submit waits them out.
|
|
20
|
+
- **Precise lifecycle control.** `reset(form, values, {keepDirtyValues, …})` covers refetch-without-clobbering-dirty-drafts, `setFocus(form, name)` focuses programmatically, and `trigger(form, name?)` resolves `Promise<boolean>` once validation settles.
|
|
21
|
+
- **Typed, nestable form contexts.** `createFormContext<Values>()` gives each app area an isolated provider whose `useField`/`useFieldArray` take `FieldPath<Values>` names without hand-written generics.
|
|
22
|
+
- **SSR out of the box.** `renderToString` renders initial values and the server snapshot matches the client's first render, so hydration is consistent.
|
|
23
|
+
- Event-driven core with refined tree-shaking — you don't pay for features you don't use.
|
|
8
24
|
|
|
9
25
|
## Install
|
|
10
26
|
|
|
@@ -16,6 +32,57 @@ or
|
|
|
16
32
|
yarn add react-f0rm
|
|
17
33
|
```
|
|
18
34
|
|
|
35
|
+
## Benchmarks
|
|
36
|
+
|
|
37
|
+
tinybench; relative margin of error ≤ 0.9% for the first three scenarios, ≤ 2.5% for the scale scenarios.
|
|
38
|
+
|
|
39
|
+
| Scenario | react-f0rm | Baseline | Speedup |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| Change one of 100 controlled fields | 113µs/change (~8,880 ops/s) | RHF `Controller`: 200µs (~5,000 ops/s) | ~1.8× |
|
|
42
|
+
| Components re-rendered per change | 1 of 100 `Field`s | — | — |
|
|
43
|
+
| `getValues()`, 100 fields × depth 3 | 42.5µs (ownership merge) | legacy chained `set`: 93.0µs | 2.19× |
|
|
44
|
+
| Change one of 1000 controlled fields | 0.418ms/change (~2,390 ops/s, rme ±1.1%) | RHF `Controller`: 1.662ms (~602 ops/s) | 4.0× |
|
|
45
|
+
| Async validation storm — burst of 3 changes × 50 debounced async validators, settled via `trigger` | 20.5ms/burst (~49 ops/s, rme ±2.4%) | — | — |
|
|
46
|
+
| `await trigger(form)` — 100 mixed validators (50 sync + 50 async) settle | 1.48ms (~676 ops/s, rme ±0.9%) | — | — |
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
|
|
50
|
+
- For reference, RHF's uncontrolled `register` — which has no per-field re-render at all — floors at 21µs/change; the controlled comparison above uses `Controller`, the fair apples-to-apples baseline.
|
|
51
|
+
- In the `getValues()` benchmark, ownership merging also cut container allocations from 300 to 111.
|
|
52
|
+
|
|
53
|
+
Reproduce with:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx vitest bench --run test/bench/render.bench.ts test/bench/getValues.bench.ts
|
|
57
|
+
npx vitest bench --run test/bench/scale.bench.ts # the three scale scenarios above
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Comparison
|
|
61
|
+
|
|
62
|
+
react-f0rm vs the established options. react-f0rm figures come from this repo (size-limit, tinybench — see [Benchmarks](#benchmarks)); competitor sizes are Bundlephobia gzip observations and drift between versions, so treat them as ballpark rather than gospel.
|
|
63
|
+
|
|
64
|
+
| | react-f0rm | React Hook Form | TanStack Form | Formik |
|
|
65
|
+
|---|---|---|---|---|
|
|
66
|
+
| Rendering model | Controlled fields with field-level subscriptions (`useSyncExternalStore`): editing one of 100 fields re-renders exactly 1 component | Uncontrolled `register` by default (no React re-render while typing); `Controller` opts into per-field re-renders | Field-level subscriptions (`form.Field` / `useField`), each field re-renders itself | Form-wide context: any state change re-renders all subscribed components |
|
|
67
|
+
| Unregister on unmount | Unregisters by default — an unmounted field drops out of `getValues()` (tombstone) instead of silently reviving its initial value; `shouldUnregister: false` keeps it | Value kept by default (`shouldUnregister` defaults to `false`); opt in per field or form to unregister on unmount | Values live in the form store; unmounting a field's UI keeps its value and state | No unregister concept — values persist until `reset` |
|
|
68
|
+
| Schema adapters | One Standard Schema entry point (`react-f0rm/resolvers/standard-schema`) covers zod, valibot, arktype, …; legacy zod/yup resolvers also shipped | `@hookform/resolvers` — one adapter module per validation library | Built-in `standardSchemaValidators` (Standard Schema v1), plus per-library adapter packages | Yup built in via `validationSchema`; other libraries hand-wired in `validate` |
|
|
69
|
+
| Path type safety | `FieldPath<T>` / `PathValue<T, P>`: every valid path enumerated, value type resolved, typos fail at compile time | `Path<T>` / `FieldPath` type-level path checking | Deep inference, including validator argument types — the strongest of the four | Top-level `keyof` only; nested paths are untyped strings |
|
|
70
|
+
| Async validation | `validateDebounce` per field + `meta.signal` (`AbortSignal`) handed to every validator — superseded rounds cancel their in-flight work; pending debounce counts as validating so submit waits | Async validators supported, but no built-in debounce and no cancellation signal — both are hand-rolled per project | Built in: `asyncDebounceMs` debounces and the validator meta carries an `AbortSignal` | Async `validate` supported; no debounce, no signal |
|
|
71
|
+
| Multiple errors per field | Native: every field holds `FieldError[]`; `getFieldErrors`/`useFieldErrors` read them; resolvers forward every schema issue | `criteriaMode: 'all'` collects all failing rules per field | Errors are arrays of messages per field | — |
|
|
72
|
+
| SSR / hydration | `renderToString` renders initial values out of the box; server snapshot matches the client's first render | SSR-safe | SSR-safe | SSR-safe |
|
|
73
|
+
| React 19 / Server Actions | Bridge pattern: dispatch the action from `onValidSubmit` via `startTransition`/`useActionState`, passing the values object rather than FormData (see the React 19 Server Actions guide); no submit before JS loads | `<Form>` accepts a function `action` prop (server-action-style submit) since v7.84, and ships a `react-server` export | Documented server action integration (`createServerValidate` for server-side validation, Next.js examples) | — |
|
|
74
|
+
| Bundle size | 11.18 KB gzip (7.1 KB brotli, minified), full core | ~11 KB gzip | ~17.5 KB gzip | ~12.8 KB gzip |
|
|
75
|
+
| Devtools | `<Devtools />` from `react-f0rm/devtools` — separate entry point, tree-shakeable, never lands in the main bundle | `@hookform/devtools` (separate package) | Built-in devtools panel | None (official) |
|
|
76
|
+
| Ecosystem maturity | New, 0.x — small audience, few integrations so far | Most mature: massive adoption, resolvers, UI-kit integrations, abundant examples and answers | Backed by the TanStack family, actively growing | Maintenance mode; the author recommends considering RHF or Final Form for new projects |
|
|
77
|
+
|
|
78
|
+
Bundle-size basis: every column is gzip. react-f0rm is measured on the local build — gzip of the shipped, unminified `dist/index.mjs` after `npm run build` (minified, the same file gzips to ~7.88 KB; 7.1 KB brotli via size-limit, which minifies and tree-shakes). Competitor figures are Bundlephobia observations of minified+gzip bundles — so ours is the conservative number, not the flattering one.
|
|
79
|
+
|
|
80
|
+
### Which one should you use?
|
|
81
|
+
|
|
82
|
+
**Pick react-f0rm** when you want controlled components with true per-field subscriptions (design systems, editor-like forms), one Standard Schema adapter instead of a package per validator, compile-time-checked paths, and a small core (11.18 KB gzip / 7.1 KB brotli) — and you are comfortable with a young 0.x library.
|
|
83
|
+
|
|
84
|
+
**Pick React Hook Form** when uncontrolled inputs are an option: its raw `register` performs no per-field re-render at all and floors at 21µs/change vs our 113µs (see [Benchmarks](#benchmarks)) — uncontrolled is simply a cheaper rendering model. RHF is also the right call when you need its mature ecosystem of resolvers, UI-library integrations and community answers today. TanStack Form sits in between: choose it when the deepest possible type inference (including validator signatures) matters more to you than bundle size.
|
|
85
|
+
|
|
19
86
|
## Usage
|
|
20
87
|
|
|
21
88
|
```jsx
|
|
@@ -46,16 +113,52 @@ For full control over field rendering:
|
|
|
46
113
|
import {useField} from 'react-f0rm';
|
|
47
114
|
|
|
48
115
|
function CustomField({name}) {
|
|
49
|
-
const {value, onChange, onBlur, error} = useField({name});
|
|
116
|
+
const {value, onChange, onBlur, error, errorObject, errors} = useField({name});
|
|
50
117
|
return (
|
|
51
118
|
<div>
|
|
52
119
|
<input value={value} onChange={e => onChange(e.target.value)} onBlur={onBlur} />
|
|
53
|
-
{error && <span>{error}</span>}
|
|
120
|
+
{error && <span role="alert">{error}</span>}
|
|
54
121
|
</div>
|
|
55
122
|
);
|
|
56
123
|
}
|
|
57
124
|
```
|
|
58
125
|
|
|
126
|
+
`error` is the error's message string (or `undefined`); `errorObject` is the full structured error `{type, message}`. `errors` is every error registered for the field (`FieldError[]`, insertion order) — `error`/`errorObject` are its first entry (see [Multiple errors per field](#multiple-errors-per-field)). Pass an explicit `form` to use the hook outside a `<Form>`:
|
|
127
|
+
|
|
128
|
+
```jsx
|
|
129
|
+
const form = useForm({initialValues: {email: ''}});
|
|
130
|
+
const {value, onChange} = useField({form, name: 'email'});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `subscribe`
|
|
134
|
+
|
|
135
|
+
Linked fields and other non-render side effects — province changed → clear city, autosave, analytics — should not require a mounted watching component. `subscribe` exposes the event core imperatively:
|
|
136
|
+
|
|
137
|
+
```jsx
|
|
138
|
+
import {createForm, subscribe, getValue, setValue} from 'react-f0rm';
|
|
139
|
+
|
|
140
|
+
const form = createForm({initialValues: {province: '', city: ''}});
|
|
141
|
+
|
|
142
|
+
const unsubscribe = subscribe(form, {
|
|
143
|
+
name: 'province',
|
|
144
|
+
callback: () => {
|
|
145
|
+
// Read fresh state through the getters inside the callback.
|
|
146
|
+
if (getValue(form, 'city')) setValue(form, 'city', '');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `name` | field path, or an array of them | omitted — every emission of `event`, payload-less broadcasts (reset, …) included |
|
|
154
|
+
| `event` | `'change'` \| `'errors'` \| `'touched'` \| `'submitting'` \| `'submitCount'` | `'change'` |
|
|
155
|
+
| `scope` | `'leaf'` \| `'branch'` | `'branch'` |
|
|
156
|
+
| `callback` | `() => void`, fired with no arguments | required |
|
|
157
|
+
|
|
158
|
+
Matching follows the event's shape. `'change'` walks the path tree: the default `'branch'` scope wakes a `'tags'` subscriber when any `tags.*` descendant is written, while `'leaf'` matches only the exact key and its ancestors. `'errors'` and `'touched'` always match the exact key — another field's error never wakes this subscriber. `'submitting'`/`'submitCount'` are payload-less, so a `name` narrows nothing. An array of names creates one subscription per path, and the returned function unsubscribes them all. A number-bearing array (`['tags', 0]`) is one segments path, not a name list — the same rule `trigger` uses.
|
|
159
|
+
|
|
160
|
+
**`subscribe` vs `useWatch`:** `useWatch` (and the `useValue`/`useError`/… readers built on it) feeds rendering — it returns a snapshot and re-renders the component when it changes. `subscribe` runs imperative code and renders nothing. Use `subscribe` for linkages and effects; reach for a hook only when the watched value itself must appear on screen.
|
|
161
|
+
|
|
59
162
|
### `useFieldArray`
|
|
60
163
|
|
|
61
164
|
Manage dynamic lists of fields:
|
|
@@ -79,6 +182,74 @@ function Tags() {
|
|
|
79
182
|
}
|
|
80
183
|
```
|
|
81
184
|
|
|
185
|
+
The array only re-renders for changes touching its own branch — typing into unrelated fields does not re-render it.
|
|
186
|
+
|
|
187
|
+
Besides the movers (`append`, `prepend`, `insert`, `remove`, `swap`, `move`), two bulk operations are available:
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
const {fields, replace, update} = useFieldArray({name: 'tags'});
|
|
191
|
+
|
|
192
|
+
replace(['a', 'b', 'c']); // full swap: every row id is regenerated (length may change)
|
|
193
|
+
update(1, 'B'); // overwrite one value, keeping that row's id — no key churn
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`replace(values)` is the refetch shape — a server response replaces the whole list — while `update(index, value)` rewrites a single row in place.
|
|
197
|
+
|
|
198
|
+
### `createFormContext`
|
|
199
|
+
|
|
200
|
+
The module-level context serves one form per subtree; nesting two forms (or reusing a component under a different form) makes them fight over it. `createFormContext` builds an isolated bundle of bindings, typed against your values shape:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import {createFormContext} from 'react-f0rm';
|
|
204
|
+
|
|
205
|
+
interface Values {
|
|
206
|
+
name: string;
|
|
207
|
+
email: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const ProfileForm = createFormContext<Values>();
|
|
211
|
+
|
|
212
|
+
function NameField() {
|
|
213
|
+
// name is constrained to FieldPath<Values>; value is inferred as string
|
|
214
|
+
const {value, onChange} = ProfileForm.useField({name: 'name'});
|
|
215
|
+
return <input value={value} onChange={e => onChange(e.target.value)} />;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Each call returns `{FormProvider, useFormContext, useField, useFieldArray}` bound to a private React context — pass the form via `<ProfileForm.FormProvider form={form}>`, and providers from separate instances never see each other's forms.
|
|
220
|
+
|
|
221
|
+
## Controlled Forms
|
|
222
|
+
|
|
223
|
+
Pass a `values` prop to `<Form>` (or `values` to `useForm`) to drive the form from outside:
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
<Form
|
|
227
|
+
values={selectedRecord}
|
|
228
|
+
onValidSubmit={values => save(values)}
|
|
229
|
+
>
|
|
230
|
+
<Field name="email" />
|
|
231
|
+
</Form>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Whenever the `values` reference changes, the new object is synced into the form: uncommitted user edits are discarded — master-detail semantics, where selecting another record replaces the draft — while touched flags and errors are kept. The sync guard is reference-first with a structural fallback: re-renders that pass the same `values` reference never re-sync, and neither does an inline literal whose content is structurally equal to what the form was last seeded from — only genuinely different content replaces the draft, so an unrelated re-render never interrupts what the user is typing.
|
|
235
|
+
|
|
236
|
+
## Disabled
|
|
237
|
+
|
|
238
|
+
Disable a whole form — during submission, while a record loads, or for read-only views:
|
|
239
|
+
|
|
240
|
+
```jsx
|
|
241
|
+
const form = useForm({disabled: isReadOnly});
|
|
242
|
+
|
|
243
|
+
// or toggle at runtime — every bound field re-renders:
|
|
244
|
+
setDisabled(form, true);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The flag is OR-ed into every bound field: `Field`, `Checkbox` and `Select` render their control disabled when either the form flag or their own `disabled` prop is true — a field cannot opt out of a disabled form. `useField` exposes the merged flag as `disabled`, kept live through the form's event core:
|
|
248
|
+
|
|
249
|
+
```jsx
|
|
250
|
+
const {disabled, value, onChange} = useField({name: 'email'});
|
|
251
|
+
```
|
|
252
|
+
|
|
82
253
|
## Submit Handlers
|
|
83
254
|
|
|
84
255
|
```jsx
|
|
@@ -90,6 +261,7 @@ function Tags() {
|
|
|
90
261
|
}}
|
|
91
262
|
onInvalidSubmit={(errors, values) => {
|
|
92
263
|
// Called when validation fails
|
|
264
|
+
// errors: [{path: 'email', type: 'custom', message: 'Invalid email'}]
|
|
93
265
|
console.error(errors);
|
|
94
266
|
}}
|
|
95
267
|
>
|
|
@@ -98,11 +270,151 @@ function Tags() {
|
|
|
98
270
|
</Form>
|
|
99
271
|
```
|
|
100
272
|
|
|
273
|
+
`onSubmit`/`onValidSubmit` only run once both native constraint validation (see [Accessibility](#accessibility)) and your custom validators pass.
|
|
274
|
+
|
|
275
|
+
### `handleSubmit`
|
|
276
|
+
|
|
277
|
+
The same submit flow is available as a standalone function — the headless counterpart of `<Form>`'s submit wiring, usable where there is no `<form>` element (React Native, toolbar buttons, …):
|
|
278
|
+
|
|
279
|
+
```jsx
|
|
280
|
+
import {useForm, handleSubmit} from 'react-f0rm';
|
|
281
|
+
|
|
282
|
+
function Profile({onSave}) {
|
|
283
|
+
const form = useForm({initialValues: {email: ''}});
|
|
284
|
+
const submit = handleSubmit(form, {
|
|
285
|
+
onSubmit: values => onSave(values), // runs first on success
|
|
286
|
+
onValidSubmit: values => console.log(values), // then this
|
|
287
|
+
onInvalidSubmit: (errors, values) => console.error(errors)
|
|
288
|
+
});
|
|
289
|
+
return <Button title="Save" onPress={submit} />;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
All callbacks are optional — a missing one is simply skipped. The returned handler runs the full submit state machine (`isSubmitting`, `submitCount`, `isSubmitSuccessful`) around native constraint validation (skipped when the event target has no `checkValidity`) and your validators, and can be invoked with or without an event object.
|
|
294
|
+
|
|
295
|
+
`onInvalidSubmit` receives an array of `{path, type, message}` entries: custom validation failures carry dotted paths with your validator's type (`'custom'` for plain strings, `'standard'` for the Standard Schema adapter), and native constraint failures carry `type: 'native'` — `path` is the dotted field path and `message` comes from the browser's `validationMessage`. Native failures are read from the DOM and never enter the form's error state.
|
|
296
|
+
|
|
297
|
+
### Focusing the first error
|
|
298
|
+
|
|
299
|
+
After a failed submit, the offending field is focused automatically — pass `shouldFocusError: false` (on `<Form>` or `handleSubmit`) to disable; it defaults to `true`. Custom validation failures focus the first errored field through a `'focusError'` event that bound fields (like `Field`) subscribe to; native constraint failures focus the submitted form's first `:invalid` control directly.
|
|
300
|
+
|
|
301
|
+
The same channel is exposed as an imperative API:
|
|
302
|
+
|
|
303
|
+
```jsx
|
|
304
|
+
import {setFocus} from 'react-f0rm';
|
|
305
|
+
|
|
306
|
+
setFocus(form, 'email'); // focus the bound field's element
|
|
307
|
+
setFocus(form, 'user.name', {shouldSelect: true}); // focus and select its text
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`setFocus` rides the `'focusError'` event, so it is a silent no-op when the field is unmounted or nothing subscribes — unknown names never throw.
|
|
311
|
+
|
|
101
312
|
## Validation
|
|
102
313
|
|
|
314
|
+
### Validation modes
|
|
315
|
+
|
|
316
|
+
`mode` controls when field validators run; `reValidateMode` controls when a field is re-validated once it already has an error — it supplements `mode` in every mode:
|
|
317
|
+
|
|
318
|
+
| Option | Values | Default |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `mode` | `'onSubmit'` \| `'onBlur'` \| `'onChange'` \| `'onTouched'` \| `'all'` | `'onSubmit'` |
|
|
321
|
+
| `reValidateMode` | `'onChange'` \| `'onBlur'` \| `'onSubmit'` | `'onChange'` |
|
|
322
|
+
|
|
323
|
+
- `'onSubmit'` — validate only on submit.
|
|
324
|
+
- `'onBlur'` — validate when the field loses focus.
|
|
325
|
+
- `'onChange'` — validate on every change.
|
|
326
|
+
- `'onTouched'` — validate on the first blur, then on every change.
|
|
327
|
+
- `'all'` — validate on both change and blur.
|
|
328
|
+
|
|
329
|
+
```jsx
|
|
330
|
+
import {createForm} from 'react-f0rm';
|
|
331
|
+
|
|
332
|
+
const form = createForm({
|
|
333
|
+
initialValues: {email: ''},
|
|
334
|
+
mode: 'onBlur', // validate on blur…
|
|
335
|
+
reValidateMode: 'onChange' // …then re-validate on every change once errored
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Triggering validation manually
|
|
340
|
+
|
|
341
|
+
`trigger` runs field validators on demand. Without a name it runs every registered validator; a single name — or an array of names — narrows it to those fields:
|
|
342
|
+
|
|
343
|
+
```jsx
|
|
344
|
+
import {trigger} from 'react-f0rm';
|
|
345
|
+
|
|
346
|
+
trigger(form); // every registered field validator
|
|
347
|
+
trigger(form, 'email'); // one field
|
|
348
|
+
trigger(form, ['user.name', 'user.email']); // several
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`trigger` returns a promise that waits for the triggered validation to settle — async validators and pending debounce windows included — so errors have already landed in `form.errors` when it resolves. It never rejects: landing errors is the expected outcome here, not a failure. It resolves `true` when the triggered scope is error-free, `false` otherwise:
|
|
352
|
+
|
|
353
|
+
```jsx
|
|
354
|
+
if (await trigger(form, 'email')) {
|
|
355
|
+
proceed(); // 'email' is now guaranteed error-free
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Without `name` the scope is all fields plus the form-level `validate` result; with `name` only those fields' own errors count and form-level `validate` is skipped (RHF semantics). Fire-and-forget callers may ignore the promise — the validator kicks still happen synchronously.
|
|
360
|
+
|
|
361
|
+
### Async validation
|
|
362
|
+
|
|
363
|
+
Async validators are first-class. Two knobs keep them cheap and race-free:
|
|
364
|
+
|
|
365
|
+
**`validateDebounce`** (on `Field`, `useField` or any bound component) delays a field's validation kicks by the given milliseconds; only the last kick inside the window runs the validator. While the timer is pending the field counts as *validating*, so `trigger` and submit wait the window out instead of racing it.
|
|
366
|
+
|
|
367
|
+
**`meta.signal`** — every validator's second argument carries `{form, path, signal}`. The `AbortSignal` fires as soon as the round is superseded (a newer round started, or the field unregistered), so async validators can cancel their underlying work instead of racing a stale result home:
|
|
368
|
+
|
|
369
|
+
```jsx
|
|
370
|
+
<Field
|
|
371
|
+
name="email"
|
|
372
|
+
validateDebounce={300}
|
|
373
|
+
validate={async (value, {signal}) => {
|
|
374
|
+
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, {signal});
|
|
375
|
+
const {taken} = await res.json();
|
|
376
|
+
if (taken) return {type: 'taken', message: 'Email already registered'};
|
|
377
|
+
}}
|
|
378
|
+
/>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Stale results are dropped independently of the signal — validators that ignore it stay correct — but passing it to `fetch` (or `AbortSignal.timeout`, timers, …) also cancels the network work itself.
|
|
382
|
+
|
|
383
|
+
### Multiple errors per field
|
|
384
|
+
|
|
385
|
+
Every field stores an ordered `FieldError[]`, not a single error. The first entry is what `error`/`errorObject`/`getError` expose; readers that want all of them use `getFieldErrors(form, name)` or `useFieldErrors(form, name)`:
|
|
386
|
+
|
|
387
|
+
```jsx
|
|
388
|
+
import {getFieldErrors, useFieldErrors, setError} from 'react-f0rm';
|
|
389
|
+
|
|
390
|
+
const all = getFieldErrors(form, 'password');
|
|
391
|
+
// [{type: 'min', message: 'Too short'}, {type: 'pattern', message: 'Needs a digit'}]
|
|
392
|
+
|
|
393
|
+
setError(form, 'password', [
|
|
394
|
+
{type: 'min', message: 'Too short'},
|
|
395
|
+
{type: 'pattern', message: 'Needs a digit'}
|
|
396
|
+
]);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`setError` accepts a string, a `FieldError`, an array mixing both, or `undefined` to clear. Schema resolvers pass every issue through — a value breaking several rules collects all of them (Standard Schema/zod by design, yup via `abortEarly: false`) — and `getErrors()` contributes one entry per error. For imperative clears, `clearErrors(form)` wipes every error while `clearErrors(form, name)` — one name or an array of names — clears only those fields.
|
|
400
|
+
|
|
401
|
+
### `setValue` options
|
|
402
|
+
|
|
403
|
+
The fourth argument to `setValue` opts into side effects. Every flag defaults to `false`; omitting the object keeps the plain set-value behavior:
|
|
404
|
+
|
|
405
|
+
```jsx
|
|
406
|
+
import {setValue} from 'react-f0rm';
|
|
407
|
+
|
|
408
|
+
setValue(form, 'email', 'a@b.com', {
|
|
409
|
+
shouldValidate: true, // run the field's registered validator after the value lands
|
|
410
|
+
shouldTouch: true, // mark the field as touched
|
|
411
|
+
shouldDirty: true // reserved for a manual dirty marker — currently a no-op
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
103
415
|
### Field-level validation
|
|
104
416
|
|
|
105
|
-
Pass a `validate` function to `Field` or `useField`. Return an error string or `undefined
|
|
417
|
+
Pass a `validate` function to `Field` or `useField`. Return an error string, a `FieldError` object or `undefined` — sync or async:
|
|
106
418
|
|
|
107
419
|
```jsx
|
|
108
420
|
<Field
|
|
@@ -113,9 +425,44 @@ Pass a `validate` function to `Field` or `useField`. Return an error string or `
|
|
|
113
425
|
/>
|
|
114
426
|
```
|
|
115
427
|
|
|
428
|
+
### Rules
|
|
429
|
+
|
|
430
|
+
For declarative constraints, pass `rules` to `Field` (or any bound component — `Checkbox`, `Select` — or `useField`). Rule failures land in the form's error state as `FieldError`s (`type` is the rule name) carrying your message, so any design system can render them uniformly instead of the browser's validity bubble:
|
|
431
|
+
|
|
432
|
+
```jsx
|
|
433
|
+
<Field
|
|
434
|
+
name="age"
|
|
435
|
+
rules={{
|
|
436
|
+
required: 'Age is required',
|
|
437
|
+
min: 18,
|
|
438
|
+
messages: {min: 'Must be an adult'}
|
|
439
|
+
}}
|
|
440
|
+
/>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
| Rule | Value | Fails when | Default message |
|
|
444
|
+
|---|---|---|---|
|
|
445
|
+
| `required` | `string \| true` | value is `''`, `undefined` or `null` (`0` and `false` count as filled) | `'This field is required'` |
|
|
446
|
+
| `min` | `number` | `Number(value) < min` — values converting to `NaN` skip the rule | `` `Must be at least ${min}` `` |
|
|
447
|
+
| `max` | `number` | `Number(value) > max` — `NaN` skips | `` `Must be at most ${max}` `` |
|
|
448
|
+
| `minLength` | `number` | a string value is shorter — non-strings skip | `` `Must be at least ${n} characters` `` |
|
|
449
|
+
| `maxLength` | `number` | a string value is longer — non-strings skip | `` `Must be at most ${n} characters` `` |
|
|
450
|
+
| `pattern` | `{value: RegExp, message: string}` | `pattern.value.test(value)` is false | the given `message` |
|
|
451
|
+
|
|
452
|
+
The optional top-level `messages` record overrides messages per rule type (`min`, `max`, `minLength`, `maxLength`, `pattern`) — useful for centralizing or localizing them.
|
|
453
|
+
|
|
454
|
+
Semantics:
|
|
455
|
+
|
|
456
|
+
- A failing `required` short-circuits the rest — an empty value reports only its `required` error, not a full panel.
|
|
457
|
+
- Every other failing rule collects into one ordered `FieldError[]` (see [Multiple errors per field](#multiple-errors-per-field)).
|
|
458
|
+
- `rules` composes with `validate`: rules run first, then `validate` (awaited when async), merging both sources' errors with rules ahead.
|
|
459
|
+
- Rules ride the exact same pipeline as `validate` — `mode`, `reValidateMode`, `validateDebounce` and `meta.signal` all apply unchanged.
|
|
460
|
+
|
|
461
|
+
Rules vs native constraints: HTML attributes (`required`, `type="email"`, `min`, …) keep running through the browser's `checkValidity`, whose bubble remains the pre-submit fallback. `rules` is the state-side alternative — failures are queryable (`getErrors`, `error`, `errors`), renderable by any UI, and carry your own messages. Prefer `rules` whenever the error text must be controlled.
|
|
462
|
+
|
|
116
463
|
### Form-level validation
|
|
117
464
|
|
|
118
|
-
Pass a `validate` function to `createForm`. It receives all values and returns
|
|
465
|
+
Pass a `validate` function to `createForm`. It receives all values and returns a record of errors. Nested objects are flattened recursively — `{user: {name: 'Required'}}` sets the error at `user.name` — and plain flat results keep working:
|
|
119
466
|
|
|
120
467
|
```jsx
|
|
121
468
|
import {createForm} from 'react-f0rm';
|
|
@@ -123,15 +470,160 @@ import {createForm} from 'react-f0rm';
|
|
|
123
470
|
const form = createForm({
|
|
124
471
|
initialValues: {password: '', confirm: ''},
|
|
125
472
|
validate: values => {
|
|
126
|
-
const errors = {};
|
|
127
473
|
if (values.password !== values.confirm) {
|
|
128
|
-
|
|
474
|
+
return {confirm: 'Passwords do not match'};
|
|
129
475
|
}
|
|
130
|
-
return errors;
|
|
131
476
|
},
|
|
132
477
|
});
|
|
133
478
|
```
|
|
134
479
|
|
|
480
|
+
### Schema validation
|
|
481
|
+
|
|
482
|
+
Any library implementing [Standard Schema v1](https://standardschema.dev) — zod v3.24+/v4, valibot v1, arktype and more — works through one adapter, imported from its own tree-shakeable entry point:
|
|
483
|
+
|
|
484
|
+
```jsx
|
|
485
|
+
import {Form, Field, createForm} from 'react-f0rm';
|
|
486
|
+
import {
|
|
487
|
+
standardSchemaFormValidator,
|
|
488
|
+
standardSchemaResolver
|
|
489
|
+
} from 'react-f0rm/resolvers/standard-schema';
|
|
490
|
+
import {z} from 'zod';
|
|
491
|
+
|
|
492
|
+
const schema = z.object({
|
|
493
|
+
email: z.string().email(),
|
|
494
|
+
password: z.string().min(8)
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Form-level: validate the whole values object; issue paths map to
|
|
498
|
+
// field errors automatically, issues without a path land on `_form`
|
|
499
|
+
const form = createForm({
|
|
500
|
+
validate: standardSchemaFormValidator(schema)
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Field-level: validate a single value
|
|
504
|
+
<Field name="email" validate={standardSchemaResolver(z.string().email())} />
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Schema errors come back as `{type: 'standard', message}`.
|
|
508
|
+
|
|
509
|
+
On success the adapter returns the schema's parsed output, which the form stores as its `parsedValues` baseline: `getValues()` and submit callbacks (`onSubmit`/`onValidSubmit`) read coerced/transformed values — `z.coerce.number()` hands back a real `number`, not the raw string. The baseline sits between `initialValues` and live edits, so fields the user changes afterwards still win, and dirty state keeps comparing live edits against `initialValues` only — parsing never marks a field dirty. `reset()` and `setInitialValues()` clear the baseline.
|
|
510
|
+
|
|
511
|
+
### Delaying error display
|
|
512
|
+
|
|
513
|
+
`delayError` (milliseconds) holds a newly appearing error back from the render for a short window — users typing through a field are not interrupted by an error the next keystroke may already fix:
|
|
514
|
+
|
|
515
|
+
```jsx
|
|
516
|
+
<Field name="username" rules={{minLength: 3}} delayError={300} />
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The delay is render-layer only: `error`/`errorObject`/`errors` from `useField` (and everything `Field` derives from them — `aria-invalid`, `renderError`) stay `undefined`/empty until the window passes. The form's error state is never delayed — `trigger`, submit and `getError(form, name)` read the error immediately, unlike react-hook-form's formState-level delay. An error that clears inside the window never shows at all; once an error is visible, later changes (a new message, entries added or removed) apply immediately — only the none → some transition waits.
|
|
520
|
+
|
|
521
|
+
## Dirty & Touched Fields
|
|
522
|
+
|
|
523
|
+
```jsx
|
|
524
|
+
import {useDirtyFields, useTouchedFields} from 'react-f0rm';
|
|
525
|
+
|
|
526
|
+
function FormStatus({form}) {
|
|
527
|
+
const dirtyFields = useDirtyFields(form); // {'user.name': true, 'tags.0': true}
|
|
528
|
+
const touched = useTouchedFields(form); // ['user.name', 'tags.0']
|
|
529
|
+
return (
|
|
530
|
+
<p>{Object.keys(dirtyFields).length} dirty, {touched.length} touched</p>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Both hooks expose user-facing dotted paths (`'a.b'`, `'a.0.c'`). The imperative counterparts `getDirtyFields(form)` and `getTouchedFields(form)` return the same shapes without subscribing.
|
|
536
|
+
|
|
537
|
+
### Resetting
|
|
538
|
+
|
|
539
|
+
`reset(form, initialValues?)` wipes values, errors, touched, tombstones and the submission flags (`isSubmitting`, `submitCount`, `isSubmitSuccessful`). The second argument installs a fresh baseline. The third opts into keeping slices of state through the reset:
|
|
540
|
+
|
|
541
|
+
```jsx
|
|
542
|
+
import {reset} from 'react-f0rm';
|
|
543
|
+
|
|
544
|
+
reset(form, freshRecord); // full reset to the new baseline
|
|
545
|
+
reset(form, freshRecord, {keepDirtyValues: true}); // dirty drafts survive
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`keepDirtyValues` is the refetch shape: reload the record from the server, but fields the user already edited keep their live values (dirtiness is measured against the pre-reset initialValues; clean fields fall back to the new baseline):
|
|
549
|
+
|
|
550
|
+
```jsx
|
|
551
|
+
const {data} = useQuery(['user', id], () => fetchUser(id));
|
|
552
|
+
// data changed (refetch, different user) — replace the draft,
|
|
553
|
+
// but never clobber fields the user is mid-edit on
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if (data) reset(form, data, {keepDirtyValues: true});
|
|
556
|
+
}, [data]);
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The other flags — `keepTouched`, `keepErrors`, `keepIsSubmitted`, `keepSubmitCount`, `keepIsSubmitting` — all default to `false`; omitting the object keeps the plain full-reset behavior.
|
|
560
|
+
|
|
561
|
+
### Resetting a single field
|
|
562
|
+
|
|
563
|
+
`resetField(form, name, options?)` resets one field and leaves the rest of the form alone: the field's live value is dropped (reads fall back to `initialValues` — when a schema's `parsedValues` baseline exists, its path is removed so the coerced output stops shadowing the initial value), and the field's touched flag and errors are cleared:
|
|
564
|
+
|
|
565
|
+
```jsx
|
|
566
|
+
import {resetField, getFieldState} from 'react-f0rm';
|
|
567
|
+
|
|
568
|
+
resetField(form, 'email'); // back to initialValues
|
|
569
|
+
resetField(form, 'email', {keepTouched: true}); // keep the touched flag
|
|
570
|
+
resetField(form, 'email', {value: ''}); // explicit value, no fallback
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
| Option | Default | Effect |
|
|
574
|
+
| ------------- | ------- | ------------------------------------------------------------- |
|
|
575
|
+
| `keepTouched` | `false` | Keep the field's touched flag |
|
|
576
|
+
| `keepErrors` | `false` | Keep the field's errors |
|
|
577
|
+
| `value` | — | Explicit post-reset value; never falls back to `initialValues` |
|
|
578
|
+
|
|
579
|
+
Its read-side sibling `getFieldState(form, name)` returns one field's aggregated state — `{value, error, errors, isDirty, isTouched, isValidating}` — where `isDirty` applies the same rule as `getDirtyFields` (a live value differing from `initialValues`; parsing never counts) and `errors` is the stored array shared with `getFieldErrors`, so treat it as read-only:
|
|
580
|
+
|
|
581
|
+
```jsx
|
|
582
|
+
const {value, error, isDirty} = getFieldState(form, 'email');
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## Accessibility
|
|
586
|
+
|
|
587
|
+
`Field` sets `aria-invalid` on the input whenever it has an error. Provide a `renderError(error, id)` function to render the message, and `Field` wraps it in `<span id={id} role="alert">` next to the input and points the input's `aria-describedby` at it:
|
|
588
|
+
|
|
589
|
+
```jsx
|
|
590
|
+
<Field
|
|
591
|
+
name="email"
|
|
592
|
+
renderError={error => <span className="field-error">{error}</span>}
|
|
593
|
+
/>
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Without `renderError`, no extra element is rendered and no `aria-describedby` is attached — the headless default stays clean.
|
|
597
|
+
|
|
598
|
+
Native constraint validation (`required`, `type=email`, `minLength`, …) gates submission: `<Form>` runs the browser's `checkValidity()` before custom validators, and failing constraints surface as native validation bubbles via `reportValidity()`.
|
|
599
|
+
|
|
600
|
+
## TypeScript
|
|
601
|
+
|
|
602
|
+
`FieldPath<T>` and `PathValue<T, P>` make field names and value types compile-time checked:
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
import {FieldPath, PathValue, useField} from 'react-f0rm';
|
|
606
|
+
|
|
607
|
+
interface Values {
|
|
608
|
+
user: {name: string};
|
|
609
|
+
tags: string[];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 'user' | 'user.name' | 'tags' | `tags.0` | `tags[0]` | ...
|
|
613
|
+
type ValuesPath = FieldPath<Values>;
|
|
614
|
+
|
|
615
|
+
// string
|
|
616
|
+
type UserName = PathValue<Values, 'user.name'>;
|
|
617
|
+
|
|
618
|
+
function UserNameField() {
|
|
619
|
+
// value is inferred as string; a typo like 'user.nmae' fails to compile
|
|
620
|
+
const {value, onChange} = useField<Values, 'user.name'>({name: 'user.name'});
|
|
621
|
+
return <input value={value} onChange={e => onChange(e.target.value)} />;
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
The same generics work on `getValue`/`setValue`/`getError` and the other path-taking helpers.
|
|
626
|
+
|
|
135
627
|
## Custom Components
|
|
136
628
|
|
|
137
629
|
Use the `as` prop to render a custom component instead of `<input>`:
|
|
@@ -144,31 +636,33 @@ function TextArea({value, onChange, ...props}) {
|
|
|
144
636
|
<Field name="bio" as={TextArea} />
|
|
145
637
|
```
|
|
146
638
|
|
|
147
|
-
|
|
639
|
+
`Select` is a controlled `<select>` — pass the options as children. A single select stores the selected option's value as a string; `multiple` stores the values of all selected options as a string array:
|
|
148
640
|
|
|
149
|
-
```
|
|
150
|
-
import {
|
|
641
|
+
```jsx
|
|
642
|
+
import {Select} from 'react-f0rm';
|
|
151
643
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
644
|
+
<Select name="country">
|
|
645
|
+
<option value="cn">China</option>
|
|
646
|
+
<option value="jp">Japan</option>
|
|
647
|
+
</Select>
|
|
156
648
|
|
|
157
|
-
|
|
649
|
+
<Select name="tags" multiple>
|
|
650
|
+
<option value="a">Tag A</option>
|
|
651
|
+
<option value="b">Tag B</option>
|
|
652
|
+
</Select>
|
|
653
|
+
```
|
|
158
654
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<Field name="name" />
|
|
169
|
-
<Field name="email" />
|
|
170
|
-
<button>Submit</button>
|
|
171
|
-
</Form>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
655
|
+
## Server-side Rendering
|
|
656
|
+
|
|
657
|
+
Form state lives in synchronously readable structures seeded from `initialValues`, and every subscription goes through `useSyncExternalStore` with a `getServerSnapshot` that computes the same snapshot as the client's first render. `renderToString` therefore renders form-driven components with their initial values out of the box, and `hydrateRoot` matches the server markup — no provider shims, no `typeof window` guards:
|
|
658
|
+
|
|
659
|
+
```jsx
|
|
660
|
+
import {renderToString} from 'react-dom/server';
|
|
661
|
+
|
|
662
|
+
// renders <input value="ada"> — then hydrates on the client without mismatches
|
|
663
|
+
const html = renderToString(<ProfileForm initialValues={{name: 'ada', city: 'london'}} />);
|
|
174
664
|
```
|
|
665
|
+
|
|
666
|
+
## Breaking changes in 0.2
|
|
667
|
+
|
|
668
|
+
v0.2 structures the error model (`FieldError`), changes unregister/reset/native-validation semantics, and more — see the [v0.1 → v0.2 migration guide](docs-site/docs/migration/v0.1-to-v0.2.md).
|