mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
docs(hooks): Add comprehensive documentation for utility hooks
Added detailed README files and CLAUDE.md updates: 1. redux/hooks-utils/README.md - Complete API reference for useTableState, useAsyncOperation, useDebounced, useThrottled - Code examples for each hook - Best practices and use cases 2. redux/hooks-forms/README.md - Complete API reference for useFormBuilder - Examples: login form, field arrays, multi-step forms, conditional fields - Best practices and validation patterns 3. CLAUDE.md updates - New "Utility Hooks" section documenting both packages - Impact analysis (eliminates ~1,500 lines of duplicate code) - Usage examples and integration patterns - Updated Redux packages count (12 total) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
45
CLAUDE.md
45
CLAUDE.md
@@ -288,12 +288,53 @@ import { useLoginLogic } from '@metabuilder/hooks/useLoginLogic'
|
||||
- Supports multi-version peer dependencies (React 18/19, Redux 8/9)
|
||||
- Location: `/hooks/` at project root
|
||||
|
||||
### Utility Hooks (`redux/hooks-utils/`, `redux/hooks-forms/`)
|
||||
|
||||
**Status**: ✅ New high-priority utilities (Jan 23, 2026)
|
||||
|
||||
**Packages Created**:
|
||||
- `@metabuilder/hooks-utils@1.0.0` - Data table, async operations, and timing utilities
|
||||
- `useTableState` - Unified data grid: pagination + sorting + filtering + search
|
||||
- `useAsyncOperation` - Non-Redux async with retry and caching
|
||||
- `useDebounced` - Value debouncing with leading/trailing options
|
||||
- `useThrottled` - Value throttling for continuous updates
|
||||
|
||||
- `@metabuilder/hooks-forms@1.0.0` - Form management with validation
|
||||
- `useFormBuilder` - Complete form state with field arrays and validation
|
||||
- Field-level and form-level error tracking
|
||||
- Touched/dirty state management
|
||||
- Submit state and error handling
|
||||
|
||||
**Impact**: Eliminates ~1,500 lines of duplicate code across codegen, workflowui, pastebin
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { useTableState, useAsyncOperation } from '@metabuilder/hooks-utils'
|
||||
import { useFormBuilder } from '@metabuilder/hooks-forms'
|
||||
|
||||
// Data grid with all operations
|
||||
const table = useTableState(items, { pageSize: 10, searchFields: ['name'] })
|
||||
table.setSearch('filter')
|
||||
table.sort('name')
|
||||
table.addFilter({ field: 'status', operator: 'eq', value: 'active' })
|
||||
|
||||
// Non-Redux async
|
||||
const { data, isLoading, execute } = useAsyncOperation(apiCall, { cacheKey: 'data' })
|
||||
|
||||
// Form with validation
|
||||
const form = useFormBuilder({ initialValues: {}, onSubmit: submitForm })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Redux State Management
|
||||
|
||||
**Current Status**: 10 packages total (including new centralized hooks)
|
||||
**Current Status**: 12 packages total (10 Redux-specific + 2 utility packages)
|
||||
|
||||
**Packages**:
|
||||
- `@metabuilder/hooks` - Centralized custom React hooks (NEW - Jan 23, 2026)
|
||||
- `@metabuilder/hooks` - Centralized custom React hooks (30 total)
|
||||
- `@metabuilder/hooks-utils` - Utility hooks with data/async helpers (NEW - Jan 23, 2026)
|
||||
- `@metabuilder/hooks-forms` - Form management hooks (NEW - Jan 23, 2026)
|
||||
- `@metabuilder/core-hooks` - Generic Redux hooks
|
||||
- `@metabuilder/api-clients` - API client hooks
|
||||
- `@metabuilder/hooks-*` - Feature-specific hooks (auth, canvas, data, core)
|
||||
|
||||
300
redux/hooks-forms/README.md
Normal file
300
redux/hooks-forms/README.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# @metabuilder/hooks-forms
|
||||
|
||||
Form management hooks for MetaBuilder with comprehensive validation, field arrays, and submission handling.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @metabuilder/hooks-forms
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### useFormBuilder
|
||||
|
||||
Complete form state management with validation and field array support.
|
||||
|
||||
```typescript
|
||||
import { useFormBuilder } from '@metabuilder/hooks-forms'
|
||||
|
||||
interface LoginForm {
|
||||
email: string
|
||||
password: string
|
||||
rememberMe: boolean
|
||||
}
|
||||
|
||||
const form = useFormBuilder<LoginForm>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
},
|
||||
validation: (values) => {
|
||||
const errors: ValidationErrors<LoginForm> = {}
|
||||
if (!values.email) {
|
||||
errors.email = 'Email is required'
|
||||
} else if (!values.email.includes('@')) {
|
||||
errors.email = 'Invalid email'
|
||||
}
|
||||
if (values.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters'
|
||||
}
|
||||
return errors
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
await loginApi(values)
|
||||
},
|
||||
validateOnBlur: true,
|
||||
validateOnChange: false,
|
||||
})
|
||||
|
||||
// In component
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); form.submit() }}>
|
||||
<div>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
value={form.values.email}
|
||||
onChange={(e) => form.setFieldValue('email', e.target.value)}
|
||||
onBlur={() => form.setFieldTouched('email')}
|
||||
/>
|
||||
{form.touched.email && form.errors.email && (
|
||||
<span className="error">{form.errors.email}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={form.values.password}
|
||||
onChange={(e) => form.setFieldValue('password', e.target.value)}
|
||||
onBlur={() => form.setFieldTouched('password')}
|
||||
/>
|
||||
{form.touched.password && form.errors.password && (
|
||||
<span className="error">{form.errors.password}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<input
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
checked={form.values.rememberMe}
|
||||
onChange={(e) => form.setFieldValue('rememberMe', e.target.checked)}
|
||||
/>
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<button type="submit" disabled={form.isSubmitting || !form.isValid}>
|
||||
{form.isSubmitting ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
|
||||
{form.submitError && (
|
||||
<div className="error">{form.submitError}</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Strongly typed form state
|
||||
- Field-level and form-level validation
|
||||
- Touched/dirty tracking per field
|
||||
- Submit state and error handling
|
||||
- Reset to initial values
|
||||
- Optimized re-renders with field-level selectors
|
||||
|
||||
---
|
||||
|
||||
## Field Arrays
|
||||
|
||||
Manage dynamic form fields with add, remove, reorder operations.
|
||||
|
||||
```typescript
|
||||
interface UserForm {
|
||||
name: string
|
||||
emails: string[]
|
||||
}
|
||||
|
||||
const form = useFormBuilder<UserForm>({
|
||||
initialValues: {
|
||||
name: 'John',
|
||||
emails: ['john@example.com'],
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
await submitForm(values)
|
||||
},
|
||||
})
|
||||
|
||||
const emailArray = form.getFieldArray('emails')
|
||||
|
||||
return (
|
||||
<form>
|
||||
{emailArray.values.map((email, index) => (
|
||||
<div key={index}>
|
||||
<input
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
const newEmails = [...emailArray.values]
|
||||
newEmails[index] = e.target.value
|
||||
form.setFieldValue('emails', newEmails)
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => emailArray.remove(index)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => emailArray.add('')}>Add Email</button>
|
||||
</form>
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### useFormBuilder
|
||||
|
||||
```typescript
|
||||
interface UseFormBuilderOptions<T> {
|
||||
initialValues: T
|
||||
validation?: (values: T) => ValidationErrors<T>
|
||||
onSubmit: (values: T) => Promise<void> | void
|
||||
validateOnBlur?: boolean
|
||||
validateOnChange?: boolean
|
||||
}
|
||||
|
||||
interface UseFormBuilderReturn<T> {
|
||||
// Values
|
||||
values: T
|
||||
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void
|
||||
setValues: (values: Partial<T>) => void
|
||||
|
||||
// Errors
|
||||
errors: ValidationErrors<T>
|
||||
getFieldError: <K extends keyof T>(field: K) => string | undefined
|
||||
hasError: <K extends keyof T>(field: K) => boolean
|
||||
|
||||
// Touched state
|
||||
touched: Partial<Record<keyof T, boolean>>
|
||||
setFieldTouched: <K extends keyof T>(field: K, isTouched?: boolean) => void
|
||||
setTouched: (touched: Partial<Record<keyof T, boolean>>) => void
|
||||
|
||||
// Dirty state
|
||||
isDirty: boolean
|
||||
dirty: Partial<Record<keyof T, boolean>>
|
||||
resetField: <K extends keyof T>(field: K) => void
|
||||
|
||||
// Submission
|
||||
submit: () => Promise<void>
|
||||
isSubmitting: boolean
|
||||
submitError: string | null
|
||||
|
||||
// Form state
|
||||
reset: () => void
|
||||
isValid: boolean
|
||||
isValidating: boolean
|
||||
|
||||
// Field arrays
|
||||
getFieldArray: <K extends keyof T>(
|
||||
field: K
|
||||
) => T[K] extends any[] ? FormFieldArray<T[K][number]> : never
|
||||
}
|
||||
|
||||
interface FormFieldArray<T> {
|
||||
values: T[]
|
||||
add: (value: T) => void
|
||||
remove: (index: number) => void
|
||||
insert: (index: number, value: T) => void
|
||||
move: (fromIndex: number, toIndex: number) => void
|
||||
clear: () => void
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Validation timing**: Use `validateOnBlur: true` for better UX, `validateOnChange: false` to reduce noise
|
||||
2. **Field arrays**: Always pass the updated array directly to `setFieldValue` for proper state updates
|
||||
3. **Error display**: Only show errors when field is touched to avoid overwhelming users
|
||||
4. **Custom validation**: Return an empty errors object for valid forms
|
||||
5. **Async validation**: Implement in `onSubmit` after synchronous validation passes
|
||||
|
||||
## Examples
|
||||
|
||||
### Multi-step form
|
||||
|
||||
```typescript
|
||||
const [step, setStep] = useState(1)
|
||||
|
||||
const form = useFormBuilder({
|
||||
initialValues: { name: '', email: '', password: '' },
|
||||
validation: (values) => {
|
||||
const errors: ValidationErrors = {}
|
||||
if (step === 1) {
|
||||
if (!values.name) errors.name = 'Required'
|
||||
} else if (step === 2) {
|
||||
if (!values.email) errors.email = 'Required'
|
||||
} else if (step === 3) {
|
||||
if (values.password.length < 8) errors.password = 'Min 8 chars'
|
||||
}
|
||||
return errors
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
await submitForm(values)
|
||||
},
|
||||
})
|
||||
|
||||
const canProceed = () => {
|
||||
// Validate current step only
|
||||
if (step === 1) return !!form.values.name
|
||||
if (step === 2) return !!form.values.email
|
||||
if (step === 3) return form.values.password.length >= 8
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional fields
|
||||
|
||||
```typescript
|
||||
const form = useFormBuilder({
|
||||
initialValues: { userType: 'customer', companyName: '' },
|
||||
validation: (values) => {
|
||||
const errors: ValidationErrors = {}
|
||||
if (values.userType === 'business' && !values.companyName) {
|
||||
errors.companyName = 'Company name required for business accounts'
|
||||
}
|
||||
return errors
|
||||
},
|
||||
onSubmit: async (values) => {},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<select
|
||||
value={form.values.userType}
|
||||
onChange={(e) => form.setFieldValue('userType', e.target.value)}
|
||||
>
|
||||
<option value="customer">Customer</option>
|
||||
<option value="business">Business</option>
|
||||
</select>
|
||||
|
||||
{form.values.userType === 'business' && (
|
||||
<input
|
||||
value={form.values.companyName}
|
||||
onChange={(e) => form.setFieldValue('companyName', e.target.value)}
|
||||
placeholder="Company name"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Packages
|
||||
|
||||
- `@metabuilder/hooks-utils` - Table and async operation hooks
|
||||
- `@metabuilder/hooks` - Core custom hooks
|
||||
253
redux/hooks-utils/README.md
Normal file
253
redux/hooks-utils/README.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# @metabuilder/hooks-utils
|
||||
|
||||
Utility hooks library for MetaBuilder - data grid operations, async management, and timing utilities.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @metabuilder/hooks-utils
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### useTableState
|
||||
|
||||
Unified data grid state management combining pagination, sorting, filtering, and searching.
|
||||
|
||||
```typescript
|
||||
import { useTableState } from '@metabuilder/hooks-utils'
|
||||
|
||||
const users = [
|
||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
||||
]
|
||||
|
||||
const table = useTableState(users, {
|
||||
pageSize: 10,
|
||||
searchFields: ['name', 'email'],
|
||||
defaultSort: { field: 'name', direction: 'asc' }
|
||||
})
|
||||
|
||||
// Use in component
|
||||
<input
|
||||
placeholder="Search..."
|
||||
value={table.search}
|
||||
onChange={(e) => table.setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<button onClick={() => table.sort('name')}>
|
||||
Sort by Name {table.sort?.field === 'name' && table.sort?.direction}
|
||||
</button>
|
||||
|
||||
<button onClick={() => table.addFilter({ field: 'status', operator: 'eq', value: 'active' })}>
|
||||
Filter Active
|
||||
</button>
|
||||
|
||||
// Render paginated results
|
||||
{table.paginatedItems.map(user => (
|
||||
<div key={user.id}>{user.name}</div>
|
||||
))}
|
||||
|
||||
<button onClick={table.prevPage} disabled={table.currentPage === 1}>Prev</button>
|
||||
<span>Page {table.currentPage} of {table.totalPages}</span>
|
||||
<button onClick={table.nextPage} disabled={table.currentPage === table.totalPages}>Next</button>
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Multi-column sorting with ascending/descending
|
||||
- Multi-filter with operators: `eq`, `contains`, `startsWith`, `endsWith`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`
|
||||
- Full-text search across specified fields
|
||||
- Configurable page size
|
||||
- Reset to initial state
|
||||
|
||||
---
|
||||
|
||||
### useAsyncOperation
|
||||
|
||||
Non-Redux async operation management with automatic retry and response caching.
|
||||
|
||||
```typescript
|
||||
import { useAsyncOperation } from '@metabuilder/hooks-utils'
|
||||
|
||||
const { data, isLoading, error, execute, retry } = useAsyncOperation(
|
||||
() => fetch('/api/users').then(r => r.json()),
|
||||
{
|
||||
retryCount: 3,
|
||||
retryDelay: 1000,
|
||||
cacheKey: 'users',
|
||||
cacheTTL: 60000, // 1 minute
|
||||
onSuccess: (data) => console.log('Loaded:', data),
|
||||
onError: (error) => console.error('Failed:', error.message),
|
||||
}
|
||||
)
|
||||
|
||||
// Auto-execute on mount
|
||||
useEffect(() => {
|
||||
execute()
|
||||
}, [execute])
|
||||
|
||||
if (isLoading) return <Spinner />
|
||||
if (error) return <Error message={error.message} onRetry={retry} />
|
||||
return <UserList users={data} />
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Automatic retry with exponential backoff
|
||||
- Response caching with TTL
|
||||
- Request deduplication
|
||||
- Status tracking: `idle`, `pending`, `succeeded`, `failed`
|
||||
- Error handling with typed errors
|
||||
- Abort capability
|
||||
|
||||
---
|
||||
|
||||
### useDebounced
|
||||
|
||||
Debounces a value with optional leading/trailing edge control.
|
||||
|
||||
```typescript
|
||||
import { useDebounced } from '@metabuilder/hooks-utils'
|
||||
|
||||
const [value, setValue] = useState('')
|
||||
const { value: debouncedValue, cancel, isPending } = useDebounced(value, 300, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue) {
|
||||
searchApi(debouncedValue)
|
||||
}
|
||||
}, [debouncedValue])
|
||||
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Type to search..."
|
||||
/>
|
||||
{isPending && <Spinner />}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Configurable delay
|
||||
- Leading/trailing edge options
|
||||
- Cancel pending debounce
|
||||
- isPending state tracking
|
||||
|
||||
---
|
||||
|
||||
### useThrottled
|
||||
|
||||
Throttles a value to emit at most once per interval.
|
||||
|
||||
```typescript
|
||||
import { useThrottled } from '@metabuilder/hooks-utils'
|
||||
|
||||
const scrollY = useWindowScroll()
|
||||
const { value: throttledY } = useThrottled(scrollY, 100, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
})
|
||||
|
||||
// Fires at most every 100ms
|
||||
useEffect(() => {
|
||||
updateScrollIndicator(throttledY)
|
||||
}, [throttledY])
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Scroll handlers
|
||||
- Resize listeners
|
||||
- Drag operations
|
||||
- Real-time updates
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### useTableState
|
||||
|
||||
```typescript
|
||||
interface UseTableStateOptions<T> {
|
||||
searchFields?: (keyof T)[]
|
||||
pageSize?: number
|
||||
defaultSort?: SortConfig<T>
|
||||
defaultFilters?: Filter<T>[]
|
||||
defaultSearch?: string
|
||||
}
|
||||
|
||||
interface UseTableStateReturn<T> {
|
||||
items: T[]
|
||||
filteredItems: T[]
|
||||
paginatedItems: T[]
|
||||
totalItems: number
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
setPage: (page: number) => void
|
||||
setPageSize: (size: number) => void
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
goToFirstPage: () => void
|
||||
goToLastPage: () => void
|
||||
sort: SortConfig<T> | null
|
||||
sort: (field: keyof T, direction?: 'asc' | 'desc') => void
|
||||
clearSort: () => void
|
||||
filters: Filter<T>[]
|
||||
addFilter: (filter: Filter<T>) => void
|
||||
removeFilter: (index: number) => void
|
||||
updateFilter: (index: number, filter: Filter<T>) => void
|
||||
clearFilters: () => void
|
||||
search: string
|
||||
setSearch: (query: string) => void
|
||||
clearSearch: () => void
|
||||
reset: () => void
|
||||
hasActiveFilters: boolean
|
||||
hasSearch: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### useAsyncOperation
|
||||
|
||||
```typescript
|
||||
interface UseAsyncOperationOptions {
|
||||
retryCount?: number
|
||||
retryDelay?: number
|
||||
retryBackoff?: number
|
||||
cacheKey?: string
|
||||
cacheTTL?: number
|
||||
onSuccess?: <T>(data: T) => void
|
||||
onError?: (error: AsyncError) => void
|
||||
onStatusChange?: (status: AsyncStatus) => void
|
||||
autoExecute?: boolean
|
||||
}
|
||||
|
||||
interface UseAsyncOperationReturn<T> {
|
||||
data: T | null
|
||||
error: AsyncError | null
|
||||
status: AsyncStatus
|
||||
isLoading: boolean
|
||||
isIdle: boolean
|
||||
isSuccess: boolean
|
||||
isError: boolean
|
||||
execute: () => Promise<T | null>
|
||||
retry: () => Promise<T | null>
|
||||
refetch: () => Promise<T | null>
|
||||
reset: () => void
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **useTableState**: Use for any list/table UI that needs sorting, filtering, or pagination
|
||||
2. **useAsyncOperation**: Use instead of manual Promise handling for non-Redux apps
|
||||
3. **useDebounced**: Use for form inputs, search fields, and value updates
|
||||
4. **useThrottled**: Use for scroll, resize, and drag listeners
|
||||
|
||||
## Related Packages
|
||||
|
||||
- `@metabuilder/hooks-forms` - Form management with validation
|
||||
- `@metabuilder/hooks` - Core custom hooks
|
||||
- `@metabuilder/redux-slices` - Redux state management
|
||||
Reference in New Issue
Block a user