Mastering Form State Management: Create a useController Hook in React
Forms are a fundamental part of most web applications, and managing their state can quickly become complex. This challenge focuses on building a reusable useController hook in React using TypeScript. This hook will streamline the process of managing individual form field state, providing a clean API for handling values, errors, and interaction states, mirroring the functionality of libraries like react-hook-form but for you to build from scratch.
Problem Description
Your task is to create a custom React hook named useController that manages the state of a single form field. This hook should be generic enough to handle various input types and provide a clear interface for interacting with form elements.
Key Requirements:
- State Management: The hook must manage the
valueof the form field. - Error Handling: It should allow for associating an
errormessage with the field. - Interaction State: The hook needs to track the
isTouchedandisDirtystates of the field.isTouched: Becomestruewhen the field has been focused and then blurred at least once.isDirty: Becomestruewhen the field's value has changed from its initial value.
- Reset Functionality: Provide a
resetfunction to set the field back to its initial state. - Value Update: A mechanism to update the field's value.
- Type Safety: Utilize TypeScript generics to ensure type safety for the field's value.
Expected Behavior:
When used with a form input (like <input>, <textarea>, etc.), the useController hook should provide props that can be easily spread onto the input element. These props should handle onChange, onBlur, and value.
Edge Cases:
- Consider how to handle initial values.
- Ensure that
isDirtyis correctly calculated based on the initial value. - Think about how the
resetfunction should behave.
Examples
Example 1: Basic Input Usage
Let's assume we have an input field for a username.
import React from 'react';
import { useController } from './useController'; // Assuming your hook is in './useController'
function UsernameInput() {
const { field, fieldState } = useController<string>({
name: 'username',
initialValue: '',
});
return (
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
type="text"
{...field} // Spreads value, onChange, onBlur
aria-invalid={fieldState.error ? 'true' : 'false'}
/>
{fieldState.error && <p style={{ color: 'red' }}>{fieldState.error}</p>}
{fieldState.isTouched && <p>Touched</p>}
{fieldState.isDirty && <p>Dirty</p>}
</div>
);
}
// In your parent component, you might call:
// const { field, fieldState } = useController({ name: 'username', initialValue: '' });
// And pass { ...field } to the input.
Output:
When the user types into the input:
field.valueupdates.fieldState.isDirtybecomestrue.
When the user focuses and then blurs the input:
fieldState.isTouchedbecomestrue.
If an error is set (e.g., via a separate setError function not shown in this simplified example):
fieldState.errorwill contain the error message.
Example 2: Resetting a Field
Consider a password input.
import React from 'react';
import { useController } from './useController';
function PasswordInput() {
const { field, fieldState, reset } = useController<string>({
name: 'password',
initialValue: 'initialPassword123',
});
return (
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
{...field}
/>
{fieldState.isDirty && <button onClick={() => reset()}>Reset Password</button>}
{fieldState.isTouched && <p>Field has been interacted with.</p>}
</div>
);
}
Output:
- Initially, the input shows "initialPassword123".
isDirtyisfalse. - If the user changes the password to "newPassword",
isDirtybecomestrue, and the "Reset Password" button appears. - Clicking "Reset Password" calls
reset(), which sets the input value back to "initialPassword123", andisDirtybecomesfalse.
Example 3: Handling Initial Value and State Transitions
import React from 'react';
import { useController } from './useController';
function AgeInput() {
const { field, fieldState } = useController<number>({
name: 'age',
initialValue: 18,
});
return (
<div>
<label htmlFor="age">Age:</label>
<input
id="age"
type="number"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))} // Ensure value is a number
/>
<p>Current Value: {field.value}</p>
<p>Is Dirty: {fieldState.isDirty.toString()}</p>
<p>Is Touched: {fieldState.isTouched.toString()}</p>
</div>
);
}
Output:
- Input shows
18.isDirtyisfalse. - User changes to
20. Input shows20.isDirtybecomestrue. - User blurs the input.
isTouchedbecomestrue. - If the user then changes the value back to
18,isDirtybecomesfalseagain.
Constraints
- The
useControllerhook must be implemented in TypeScript. - The hook should accept an
optionsobject with at leastname(string) andinitialValue(generic type T). - The hook must return an object containing
field(an object withvalue,onChange,onBlur) andfieldState(an object witherror(optional string),isTouched(boolean),isDirty(boolean)). - It should also return a
resetfunction. - The
onChangehandler provided by the hook should correctly update the value andisDirtystate. - The
onBlurhandler provided by the hook should correctly setisTouchedtotrue.
Notes
- You'll need to manage the internal state for
value,error,isTouched, andisDirty. - Consider how
isDirtyshould be calculated: it's true if the current value is different from theinitialValue. - The
resetfunction should restore thevalue,error,isTouched, andisDirtystates to their initial configuration. - Think about the signature of the
onChangehandler you receive from the input element and how to process it for your hook's internal state. - This challenge is about building the core
useControllerlogic. You do not need to implement a full form management system (like managing multiple controllers in a form context). Focus on a single controller. - Consider how you will set an error message from "outside" the hook. For this challenge, you can assume an
setErrorfunction is available and will be called elsewhere (or modify the hook's return signature if you want to expose it). For simplicity, we'll focus on the state managed by the controller.