mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 03:37:36 +00:00
shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
This commit is contained in:
1 parent
6776366ce6
commit
6406208b35
637 files changed
+100297
-497
No files matched your search
@@ -0,0 +1,416 @@
|
||||
---
|
||||
name: tanstack-form
|
||||
description: Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
|
||||
---
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).
|
||||
|
||||
**Package:** `@tanstack/react-form`
|
||||
**Adapters:** `@tanstack/zod-form-adapter`, `@tanstack/valibot-form-adapter`
|
||||
**Status:** Stable (v1)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tanstack/react-form
|
||||
# Optional schema adapters:
|
||||
npm install @tanstack/zod-form-adapter zod
|
||||
npm install @tanstack/valibot-form-adapter valibot
|
||||
```
|
||||
|
||||
## Core: useForm
|
||||
|
||||
```tsx
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
|
||||
function MyForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
age: 0,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
// value is fully typed
|
||||
await submitToServer(value)
|
||||
},
|
||||
onSubmitInvalid: ({ value, formApi }) => {
|
||||
console.log('Validation failed:', formApi.state.errors)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
{/* Fields */}
|
||||
<form.Subscribe
|
||||
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
||||
children={({ canSubmit, isSubmitting }) => (
|
||||
<button type="submit" disabled={!canSubmit}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Fields (form.Field)
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="firstName"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
value.length < 3 ? 'Must be at least 3 characters' : undefined,
|
||||
}}
|
||||
children={(field) => (
|
||||
<div>
|
||||
<label htmlFor={field.name}>First Name</label>
|
||||
<input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
{field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
|
||||
<em>{field.state.meta.errors.join(', ')}</em>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<!-- Nested fields use dot notation -->
|
||||
<form.Field name="address.city">
|
||||
{(field) => (
|
||||
<input
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Validation Timing
|
||||
|
||||
| Cause | When |
|
||||
|-------|------|
|
||||
| `onChange` | After every value change |
|
||||
| `onBlur` | When field loses focus |
|
||||
| `onSubmit` | During submission |
|
||||
| `onMount` | When field mounts |
|
||||
|
||||
### Synchronous Validation
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="age"
|
||||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (value < 18) return 'Must be 18 or older'
|
||||
return undefined // undefined = valid
|
||||
},
|
||||
onBlur: ({ value }) => {
|
||||
if (!value) return 'Required'
|
||||
return undefined
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Asynchronous Validation
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="username"
|
||||
asyncDebounceMs={500}
|
||||
validators={{
|
||||
onChangeAsync: async ({ value }) => {
|
||||
const res = await fetch(`/api/check-username?q=${value}`)
|
||||
const { available } = await res.json()
|
||||
if (!available) return 'Username taken'
|
||||
return undefined
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(field) => (
|
||||
<>
|
||||
<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
|
||||
{field.state.meta.isValidating && <span>Checking...</span>}
|
||||
</>
|
||||
)}
|
||||
</form.Field>
|
||||
```
|
||||
|
||||
### Schema Validation (Zod)
|
||||
|
||||
```tsx
|
||||
import { zodValidator } from '@tanstack/zod-form-adapter'
|
||||
import { z } from 'zod'
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: { email: '', age: 0 },
|
||||
validatorAdapter: zodValidator(),
|
||||
onSubmit: async ({ value }) => { /* ... */ },
|
||||
})
|
||||
|
||||
<form.Field
|
||||
name="email"
|
||||
validators={{
|
||||
onChange: z.string().email('Invalid email'),
|
||||
onBlur: z.string().min(1, 'Required'),
|
||||
}}
|
||||
/>
|
||||
|
||||
<form.Field
|
||||
name="age"
|
||||
validators={{
|
||||
onChange: z.number().min(18, 'Must be 18+'),
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Form-Level Validation
|
||||
|
||||
```tsx
|
||||
const form = useForm({
|
||||
defaultValues: { password: '', confirmPassword: '' },
|
||||
validators: {
|
||||
onChange: ({ value }) => {
|
||||
if (value.password !== value.confirmPassword) {
|
||||
return 'Passwords do not match'
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Linked/Dependent Fields
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="confirmPassword"
|
||||
validators={{
|
||||
onChangeListenTo: ['password'], // Re-validate when password changes
|
||||
onChange: ({ value, fieldApi }) => {
|
||||
const password = fieldApi.form.getFieldValue('password')
|
||||
if (value !== password) return 'Passwords do not match'
|
||||
return undefined
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
```tsx
|
||||
<form.Field name="people" mode="array">
|
||||
{(field) => (
|
||||
<div>
|
||||
{field.state.value.map((_, index) => (
|
||||
<div key={index}>
|
||||
<form.Field name={`people[${index}].name`}>
|
||||
{(subField) => (
|
||||
<input
|
||||
value={subField.state.value}
|
||||
onChange={(e) => subField.handleChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<button type="button" onClick={() => field.removeValue(index)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
|
||||
Add Person
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
```
|
||||
|
||||
### Array Methods
|
||||
|
||||
```typescript
|
||||
field.pushValue(item) // Add to end
|
||||
field.insertValue(index, item) // Insert at index
|
||||
field.replaceValue(index, item) // Replace at index
|
||||
field.removeValue(index) // Remove at index
|
||||
field.swapValues(indexA, indexB) // Swap positions
|
||||
field.moveValue(from, to) // Move position
|
||||
```
|
||||
|
||||
## Listeners (Side Effects)
|
||||
|
||||
```tsx
|
||||
<form.Field
|
||||
name="country"
|
||||
listeners={{
|
||||
onChange: ({ value }) => {
|
||||
// Side effect: reset dependent fields
|
||||
form.setFieldValue('state', '')
|
||||
form.setFieldValue('postalCode', '')
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Reactivity (form.Subscribe & useStore)
|
||||
|
||||
```tsx
|
||||
// Render-prop subscription (fine-grained)
|
||||
<form.Subscribe
|
||||
selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
|
||||
children={({ canSubmit, isDirty }) => (
|
||||
<div>
|
||||
{isDirty && <span>Unsaved changes</span>}
|
||||
<button disabled={!canSubmit}>Save</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
// Hook-based subscription
|
||||
function FormStatus() {
|
||||
const isValid = form.useStore((s) => s.isValid)
|
||||
return isValid ? null : <p>Fix errors</p>
|
||||
}
|
||||
```
|
||||
|
||||
## Form State
|
||||
|
||||
```typescript
|
||||
interface FormState {
|
||||
values: TFormData
|
||||
errors: ValidationError[]
|
||||
errorMap: Record<string, ValidationError>
|
||||
isFormValid: boolean
|
||||
isFieldsValid: boolean
|
||||
isValid: boolean // isFormValid && isFieldsValid
|
||||
isTouched: boolean
|
||||
isPristine: boolean
|
||||
isDirty: boolean
|
||||
isSubmitting: boolean
|
||||
isSubmitted: boolean
|
||||
isSubmitSuccessful: boolean
|
||||
submissionAttempts: number
|
||||
canSubmit: boolean // isValid && !isSubmitting
|
||||
}
|
||||
```
|
||||
|
||||
## Field State
|
||||
|
||||
```typescript
|
||||
interface FieldState<TData> {
|
||||
value: TData
|
||||
meta: {
|
||||
isTouched: boolean
|
||||
isDirty: boolean
|
||||
isPristine: boolean
|
||||
isValidating: boolean
|
||||
errors: ValidationError[]
|
||||
errorMap: Record<ValidationCause, ValidationError>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FormApi Methods
|
||||
|
||||
```typescript
|
||||
form.handleSubmit()
|
||||
form.reset()
|
||||
form.getFieldValue(field)
|
||||
form.setFieldValue(field, value)
|
||||
form.getFieldMeta(field)
|
||||
form.setFieldMeta(field, updater)
|
||||
form.validateAllFields(cause)
|
||||
form.validateField(field, cause)
|
||||
form.deleteField(field)
|
||||
```
|
||||
|
||||
## Shared Form Options (formOptions)
|
||||
|
||||
```tsx
|
||||
import { formOptions } from '@tanstack/react-form'
|
||||
|
||||
const sharedOpts = formOptions({
|
||||
defaultValues: { firstName: '', lastName: '' },
|
||||
})
|
||||
|
||||
// Reuse across components
|
||||
const form = useForm({
|
||||
...sharedOpts,
|
||||
onSubmit: async ({ value }) => { /* ... */ },
|
||||
})
|
||||
```
|
||||
|
||||
## Server-Side Validation
|
||||
|
||||
```tsx
|
||||
// TanStack Start / Next.js server action
|
||||
import { ServerValidateError } from '@tanstack/react-form/nextjs'
|
||||
|
||||
export async function validateForm(data: FormData) {
|
||||
const email = data.get('email') as string
|
||||
if (await checkEmailExists(email)) {
|
||||
throw new ServerValidateError({
|
||||
form: 'Submission failed',
|
||||
fields: { email: 'Email already registered' },
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Integration
|
||||
|
||||
```tsx
|
||||
// Type-safe field paths with DeepKeys
|
||||
interface UserForm {
|
||||
name: string
|
||||
address: { street: string; city: string }
|
||||
tags: string[]
|
||||
contacts: Array<{ name: string; phone: string }>
|
||||
}
|
||||
|
||||
// TypeScript auto-completes all valid paths:
|
||||
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
|
||||
<form.Field name="address.city" /> // OK
|
||||
<form.Field name="nonexistent" /> // Type Error!
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always call `e.preventDefault()` and `e.stopPropagation()`** on form submit
|
||||
2. **Always attach `onBlur={field.handleBlur}`** for blur validation and isTouched tracking
|
||||
3. **Use `mode="array"`** for array fields to get array methods
|
||||
4. **Return `undefined`** (not null/false) for valid validators
|
||||
5. **Use `asyncDebounceMs`** for async validators to prevent API spam
|
||||
6. **Check `isTouched` before showing errors** for better UX
|
||||
7. **Use `form.Subscribe` with selectors** to minimize re-renders
|
||||
8. **Use `formOptions`** for shared configuration across components
|
||||
9. **Use schema validators** (Zod/Valibot) for complex validation rules
|
||||
10. **Use `onChangeListenTo`** for cross-field validation dependencies
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting `e.preventDefault()` on form submit (causes page reload)
|
||||
- Not attaching `onBlur` to inputs (breaks blur validation and isTouched)
|
||||
- Returning `null` or `false` instead of `undefined` for valid fields
|
||||
- Using `mode="array"` incorrectly (only needed on the array field itself, not sub-fields)
|
||||
- Subscribing to entire form state instead of using selectors (unnecessary re-renders)
|
||||
- Not using `asyncDebounceMs` with async validators (fires on every keystroke)
|
||||
Reference in new issue
Block a user