0
# State Management
1
2
Utility hooks and functions for managing component state, particularly in controlled/uncontrolled component patterns common in form elements and interactive components.
3
4
## Capabilities
5
6
### Managed State Hook
7
8
Hook for managing component state that can be either controlled (managed by parent) or uncontrolled (managed internally), with automatic detection and warnings for improper usage patterns.
9
10
```typescript { .api }
11
/**
12
* Hook for controlled/uncontrolled component state management
13
* @param controlledValue - Value controlled by parent component
14
* @param defaultValue - Default value for uncontrolled mode
15
* @param onChange - Change handler function
16
* @returns Tuple of current value and change handler
17
*/
18
function useManagedState<V, E = ChangeEvent>(
19
controlledValue: V | undefined,
20
defaultValue: V,
21
onChange: ManagedChangeHandler<V, E> | undefined
22
): [V, ManagedChangeHandler<V, E>];
23
24
/**
25
* Type for change handler functions
26
*/
27
type ManagedChangeHandler<V = string, E = ChangeEvent> = (value: V, event: E) => void;
28
```
29
30
**Usage Examples:**
31
32
```typescript
33
import { useManagedState, ManagedChangeHandler } from "@keystone-ui/core";
34
35
// Custom input component supporting controlled and uncontrolled modes
36
interface CustomInputProps {
37
value?: string;
38
defaultValue?: string;
39
onChange?: ManagedChangeHandler<string>;
40
placeholder?: string;
41
}
42
43
function CustomInput({
44
value: controlledValue,
45
defaultValue = '',
46
onChange,
47
placeholder
48
}: CustomInputProps) {
49
const [value, setValue] = useManagedState(
50
controlledValue,
51
defaultValue,
52
onChange
53
);
54
55
return (
56
<input
57
value={value}
58
onChange={(e) => setValue(e.target.value, e)}
59
placeholder={placeholder}
60
/>
61
);
62
}
63
64
// Controlled usage
65
function ControlledExample() {
66
const [inputValue, setInputValue] = useState('');
67
68
return (
69
<CustomInput
70
value={inputValue}
71
onChange={(value) => setInputValue(value)}
72
/>
73
);
74
}
75
76
// Uncontrolled usage
77
function UncontrolledExample() {
78
return (
79
<CustomInput
80
defaultValue="initial value"
81
onChange={(value) => console.log('Changed to:', value)}
82
/>
83
);
84
}
85
86
// Complex value types
87
interface ToggleProps {
88
checked?: boolean;
89
defaultChecked?: boolean;
90
onChange?: ManagedChangeHandler<boolean, MouseEvent>;
91
}
92
93
function Toggle({ checked: controlledChecked, defaultChecked = false, onChange }: ToggleProps) {
94
const [checked, setChecked] = useManagedState(
95
controlledChecked,
96
defaultChecked,
97
onChange
98
);
99
100
return (
101
<button
102
role="switch"
103
aria-checked={checked}
104
onClick={(e) => setChecked(!checked, e)}
105
>
106
{checked ? 'On' : 'Off'}
107
</button>
108
);
109
}
110
```
111
112
**Key Features:**
113
114
- **Automatic Mode Detection**: Detects controlled vs uncontrolled based on initial prop values
115
- **Development Warnings**: Warns when components switch between controlled/uncontrolled modes
116
- **Type Safety**: Full TypeScript support for value and event types
117
- **Flexible Events**: Supports any event type, not just React ChangeEvents
118
119
## Utility Functions
120
121
### Development Warning Function
122
123
Development-only warning utility that logs messages to console when conditions are met, automatically disabled in production builds.
124
125
```typescript { .api }
126
/**
127
* Logs warning message in development mode only
128
* @param condition - When true, the warning will be logged
129
* @param message - Warning message to display
130
*/
131
function devWarning(condition: boolean, message: string): void;
132
```
133
134
**Usage Examples:**
135
136
```typescript
137
import { devWarning } from "@keystone-ui/core";
138
139
function FormField({ required, value, onChange }) {
140
// Warn about missing required props
141
devWarning(
142
required && !value,
143
'FormField: required field is empty'
144
);
145
146
devWarning(
147
value !== undefined && typeof onChange !== 'function',
148
'FormField: controlled field is missing onChange handler'
149
);
150
151
// Warn about deprecated patterns
152
devWarning(
153
props.hasOwnProperty('color') && props.hasOwnProperty('variant'),
154
'FormField: color prop is deprecated, use variant instead'
155
);
156
157
return <input value={value} onChange={onChange} required={required} />;
158
}
159
160
// Custom hook with validation warnings
161
function useFormValidation(schema, values) {
162
devWarning(
163
!schema,
164
'useFormValidation: schema is required'
165
);
166
167
devWarning(
168
Object.keys(values).length === 0,
169
'useFormValidation: no values provided for validation'
170
);
171
172
// Validation logic...
173
}
174
```
175
176
### ID Generation Utilities
177
178
Utilities for generating unique, accessible IDs with server-side rendering support and compound ID creation.
179
180
```typescript { .api }
181
/**
182
* Hook for generating unique IDs with SSR support
183
* @param idFromProps - Optional ID provided via props
184
* @returns Unique string ID or undefined during SSR
185
*/
186
function useId(idFromProps?: string | null): string | undefined;
187
188
/**
189
* Creates compound ID from multiple inputs, filtering out null/undefined values
190
* @param args - Values to combine into compound ID
191
* @returns Compound ID string with components joined by '--'
192
*/
193
function makeId(...args: (string | number | null | undefined)[]): string;
194
```
195
196
**Usage Examples:**
197
198
```typescript
199
import { useId, makeId } from "@keystone-ui/core";
200
201
// Form field with auto-generated IDs
202
function FormField({
203
id: providedId,
204
label,
205
helperText,
206
errorMessage,
207
required
208
}) {
209
const baseId = useId(providedId);
210
const inputId = baseId;
211
const labelId = makeId(baseId, 'label');
212
const helperId = makeId(baseId, 'helper');
213
const errorId = makeId(baseId, 'error');
214
215
return (
216
<div>
217
<label id={labelId} htmlFor={inputId}>
218
{label} {required && '*'}
219
</label>
220
221
<input
222
id={inputId}
223
aria-labelledby={labelId}
224
aria-describedby={makeId(
225
helperText && helperId,
226
errorMessage && errorId
227
)}
228
aria-invalid={!!errorMessage}
229
aria-required={required}
230
/>
231
232
{helperText && (
233
<div id={helperId} className="helper-text">
234
{helperText}
235
</div>
236
)}
237
238
{errorMessage && (
239
<div id={errorId} className="error-text" role="alert">
240
{errorMessage}
241
</div>
242
)}
243
</div>
244
);
245
}
246
247
// Modal with related IDs
248
function Modal({ titleId: providedTitleId, children }) {
249
const baseId = useId();
250
const titleId = useId(providedTitleId) || makeId(baseId, 'title');
251
const contentId = makeId(baseId, 'content');
252
253
return (
254
<div
255
role="dialog"
256
aria-labelledby={titleId}
257
aria-describedby={contentId}
258
>
259
<h2 id={titleId}>Modal Title</h2>
260
<div id={contentId}>
261
{children}
262
</div>
263
</div>
264
);
265
}
266
267
// Compound IDs with filtering
268
makeId('modal', null, 'content'); // 'modal--content'
269
makeId('field', 123, undefined, 'input'); // 'field--123--input'
270
makeId(null, undefined); // ''
271
```
272
273
### SSR-Safe Layout Effect Hook
274
275
Hook that provides SSR-safe useLayoutEffect functionality, automatically falling back to useEffect in server environments.
276
277
```typescript { .api }
278
/**
279
* SSR-safe version of useLayoutEffect
280
* Falls back to no-op on server, useLayoutEffect on client
281
*/
282
const useSafeLayoutEffect: typeof useLayoutEffect;
283
```
284
285
**Usage Examples:**
286
287
```typescript
288
import { useSafeLayoutEffect } from "@keystone-ui/core";
289
290
// Measuring DOM elements safely
291
function AutoResizeTextarea({ value, onChange }) {
292
const textareaRef = useRef<HTMLTextAreaElement>(null);
293
294
useSafeLayoutEffect(() => {
295
if (textareaRef.current) {
296
// Reset height to get accurate scrollHeight
297
textareaRef.current.style.height = 'auto';
298
// Set height to content height
299
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
300
}
301
}, [value]);
302
303
return (
304
<textarea
305
ref={textareaRef}
306
value={value}
307
onChange={onChange}
308
style={{ resize: 'none', overflow: 'hidden' }}
309
/>
310
);
311
}
312
313
// Focus management after render
314
function FocusOnMount({ autoFocus, children }) {
315
const elementRef = useRef<HTMLElement>(null);
316
317
useSafeLayoutEffect(() => {
318
if (autoFocus && elementRef.current) {
319
elementRef.current.focus();
320
}
321
}, [autoFocus]);
322
323
return React.cloneElement(children, { ref: elementRef });
324
}
325
```
326
327
## Advanced State Management Patterns
328
329
### Compound Component State
330
331
Using managed state for compound component patterns:
332
333
```typescript
334
function TabsProvider({ selectedTab, defaultSelectedTab, onTabChange, children }) {
335
const [activeTab, setActiveTab] = useManagedState(
336
selectedTab,
337
defaultSelectedTab || 0,
338
onTabChange
339
);
340
341
const contextValue = {
342
activeTab,
343
setActiveTab,
344
registerTab: useCallback((index) => {
345
// Tab registration logic
346
}, [])
347
};
348
349
return (
350
<TabsContext.Provider value={contextValue}>
351
{children}
352
</TabsContext.Provider>
353
);
354
}
355
356
function Tab({ index, children }) {
357
const { activeTab, setActiveTab } = useContext(TabsContext);
358
const id = useId();
359
const panelId = makeId(id, 'panel');
360
361
return (
362
<button
363
id={id}
364
role="tab"
365
aria-selected={activeTab === index}
366
aria-controls={panelId}
367
onClick={() => setActiveTab(index)}
368
>
369
{children}
370
</button>
371
);
372
}
373
```
374
375
### Form State Management
376
377
Combining multiple managed state hooks for complex forms:
378
379
```typescript
380
function useFormState(initialValues, validationSchema) {
381
const [values, setValues] = useState(initialValues);
382
const [errors, setErrors] = useState({});
383
const [touched, setTouched] = useState({});
384
385
const createFieldProps = useCallback((name) => {
386
const fieldId = useId();
387
const errorId = makeId(fieldId, 'error');
388
389
return {
390
id: fieldId,
391
value: values[name] || '',
392
onChange: (value) => {
393
setValues(prev => ({ ...prev, [name]: value }));
394
// Validate on change...
395
},
396
onBlur: () => {
397
setTouched(prev => ({ ...prev, [name]: true }));
398
// Validate on blur...
399
},
400
'aria-invalid': !!(errors[name] && touched[name]),
401
'aria-describedby': errors[name] && touched[name] ? errorId : undefined
402
};
403
}, [values, errors, touched]);
404
405
return { values, errors, touched, createFieldProps };
406
}
407
```
408
409
## Best Practices
410
411
### State Management Guidelines
412
413
1. **Use useManagedState for dual-mode components** that need to work both controlled and uncontrolled
414
2. **Provide meaningful default values** to ensure consistent initial state
415
3. **Use development warnings** to guide proper component usage
416
4. **Generate stable IDs** for accessibility relationships
417
5. **Handle SSR properly** with useSafeLayoutEffect for DOM measurements
418
419
### Performance Considerations
420
421
- useManagedState creates stable change handlers to prevent unnecessary re-renders
422
- useId generates IDs only once per component instance
423
- devWarning calls are automatically stripped from production builds
424
- useSafeLayoutEffect avoids server/client mismatches