Hone logo
Hone
Problems

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 value of the form field.
  • Error Handling: It should allow for associating an error message with the field.
  • Interaction State: The hook needs to track the isTouched and isDirty states of the field.
    • isTouched: Becomes true when the field has been focused and then blurred at least once.
    • isDirty: Becomes true when the field's value has changed from its initial value.
  • Reset Functionality: Provide a reset function 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 isDirty is correctly calculated based on the initial value.
  • Think about how the reset function 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.value updates.
  • fieldState.isDirty becomes true.

When the user focuses and then blurs the input:

  • fieldState.isTouched becomes true.

If an error is set (e.g., via a separate setError function not shown in this simplified example):

  • fieldState.error will 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:

  1. Initially, the input shows "initialPassword123". isDirty is false.
  2. If the user changes the password to "newPassword", isDirty becomes true, and the "Reset Password" button appears.
  3. Clicking "Reset Password" calls reset(), which sets the input value back to "initialPassword123", and isDirty becomes false.

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:

  1. Input shows 18. isDirty is false.
  2. User changes to 20. Input shows 20. isDirty becomes true.
  3. User blurs the input. isTouched becomes true.
  4. If the user then changes the value back to 18, isDirty becomes false again.

Constraints

  • The useController hook must be implemented in TypeScript.
  • The hook should accept an options object with at least name (string) and initialValue (generic type T).
  • The hook must return an object containing field (an object with value, onChange, onBlur) and fieldState (an object with error (optional string), isTouched (boolean), isDirty (boolean)).
  • It should also return a reset function.
  • The onChange handler provided by the hook should correctly update the value and isDirty state.
  • The onBlur handler provided by the hook should correctly set isTouched to true.

Notes

  • You'll need to manage the internal state for value, error, isTouched, and isDirty.
  • Consider how isDirty should be calculated: it's true if the current value is different from the initialValue.
  • The reset function should restore the value, error, isTouched, and isDirty states to their initial configuration.
  • Think about the signature of the onChange handler you receive from the input element and how to process it for your hook's internal state.
  • This challenge is about building the core useController logic. 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 setError function 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.
Loading editor...
typescript