Hone logo
Hone
Problems

React useForm Hook Challenge

This challenge asks you to build a custom React hook, useForm, that simplifies form management in React applications. A well-built useForm hook should abstract away common form logic such as handling input changes, managing form state, validating fields, and submitting the form, allowing developers to focus on UI and business logic.

Problem Description

Your task is to create a useForm hook in TypeScript that provides the following functionalities:

  1. State Management:

    • Manage the state of form values. The initial values should be configurable.
    • Provide a way to access and update these form values.
  2. Input Handling:

    • Provide a function to handle onChange events for form inputs. This function should automatically update the corresponding form value based on the input's name and value (or checked for checkboxes).
  3. Validation:

    • Allow defining validation rules for individual form fields.
    • Provide a mechanism to trigger validation.
    • Store and expose validation errors.
  4. Submission:

    • Provide a function to handle form submission. This function should:
      • Prevent the default form submission behavior.
      • Trigger validation before submitting.
      • If validation passes, call a user-provided onSubmit handler with the form values.
      • If validation fails, update the errors state.
  5. Reset:

    • Provide a function to reset the form to its initial values and clear any errors.

Key Requirements:

  • The hook should be generic, accepting a type T for the form values.
  • The hook should return an object containing:
    • values: The current form values.
    • errors: An object containing validation errors for each field.
    • handleChange: A function to handle input change events.
    • handleSubmit: A function to handle form submission.
    • resetForm: A function to reset the form.
    • setFieldValue: A function to manually set a specific field's value.
    • setErrors: A function to manually set validation errors.

Expected Behavior:

  • When an input's value changes, the corresponding field in the values state should be updated.
  • When handleSubmit is called, validation should run. If any errors are found, errors state should be updated, and the onSubmit handler should not be called.
  • If validation passes, the onSubmit handler should be called with the current values.
  • resetForm should restore values to the initial state and clear errors.
  • setFieldValue should update a single field's value.
  • setErrors should update the errors state directly.

Edge Cases:

  • Handling different input types (text, number, checkbox, radio buttons).
  • Form fields with no validation rules.
  • Empty initial values.

Examples

Example 1: Basic Form Usage

// Assuming you have a component that uses the useForm hook like this:
const initialValues = {
  firstName: '',
  lastName: '',
};

const validationRules = {
  firstName: (value: string) => (value ? '' : 'First Name is required'),
  lastName: (value: string) => (value ? '' : 'Last Name is required'),
};

function MyForm() {
  const { values, errors, handleChange, handleSubmit, resetForm } = useForm(initialValues, validationRules);

  const onSubmit = (data: typeof initialValues) => {
    console.log('Form submitted:', data);
    // Perform actual submission logic here
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="firstName">First Name:</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          value={values.firstName}
          onChange={handleChange}
        />
        {errors.firstName && <p style={{ color: 'red' }}>{errors.firstName}</p>}
      </div>
      <div>
        <label htmlFor="lastName">Last Name:</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          value={values.lastName}
          onChange={handleChange}
        />
        {errors.lastName && <p style={{ color: 'red' }}>{errors.lastName}</p>}
      </div>
      <button type="submit">Submit</button>
      <button type="button" onClick={resetForm}>Reset</button>
    </form>
  );
}

Input to the useForm hook (initial values and rules):

const initialValues = {
  firstName: '',
  lastName: '',
};

const validationRules = {
  firstName: (value: string) => (value ? '' : 'First Name is required'),
  lastName: (value: string) => (value ? '' : 'Last Name is required'),
};

User Interaction and Expected values and errors state changes:

  1. Initial Load:

    • values: { firstName: '', lastName: '' }
    • errors: {}
  2. User types "John" into First Name:

    • values: { firstName: 'John', lastName: '' }
    • errors: {}
  3. User clicks Submit without filling Last Name:

    • values remains { firstName: 'John', lastName: '' }
    • errors becomes { lastName: 'Last Name is required' }
  4. User types "Doe" into Last Name and clicks Submit:

    • values: { firstName: 'John', lastName: 'Doe' }
    • errors: {}
    • onSubmit handler is called with { firstName: 'John', lastName: 'Doe' }.
  5. User clicks Reset:

    • values: { firstName: '', lastName: '' }
    • errors: {}

Example 2: Checkbox and Manual Field Setting

// Component using useForm
const initialValues = {
  agreeToTerms: false,
  email: '',
};

const validationRules = {
  email: (value: string) => {
    if (!value) return 'Email is required';
    if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
    return '';
  },
};

function RegistrationForm() {
  const {
    values,
    errors,
    handleChange,
    handleSubmit,
    setFieldValue, // Added for manual setting
  } = useForm(initialValues, validationRules);

  const onSubmit = (data: typeof initialValues) => {
    console.log('Registration submitted:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          type="checkbox"
          id="agreeToTerms"
          name="agreeToTerms"
          checked={values.agreeToTerms} // Use 'checked' for checkboxes
          onChange={handleChange}
        />
        <label htmlFor="agreeToTerms">I agree to the terms</label>
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      <button type="submit">Register</button>
      <button
        type="button"
        onClick={() => setFieldValue('email', 'prefilled@example.com')}
      >
        Prefill Email
      </button>
    </form>
  );
}

Input to the useForm hook:

const initialValues = {
  agreeToTerms: false,
  email: '',
};

const validationRules = {
  email: (value: string) => {
    if (!value) return 'Email is required';
    if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
    return '';
  },
};

User Interaction and Expected State Changes:

  1. Initial Load:

    • values: { agreeToTerms: false, email: '' }
    • errors: {}
  2. User checks the "agreeToTerms" checkbox:

    • values: { agreeToTerms: true, email: '' }
    • errors: {}
  3. User clicks "Prefill Email" button:

    • values: { agreeToTerms: true, email: 'prefilled@example.com' }
    • errors: {}
  4. User clears email and clicks Submit:

    • values: { agreeToTerms: true, email: '' }
    • errors: { email: 'Email is required' }

Constraints

  • The useForm hook must be implemented using TypeScript.
  • The hook should be performant and avoid unnecessary re-renders.
  • The handleChange function must correctly infer the input type (e.g., event.target.value for text inputs, event.target.checked for checkboxes).
  • The validationRules object should have keys matching the field names in initialValues.
  • The onSubmit handler passed to handleSubmit will receive the current values object.

Notes

  • Consider how to handle deeply nested form values if needed (though for this challenge, flat structures are sufficient).
  • The handleChange function needs to correctly identify whether to use event.target.value or event.target.checked.
  • Think about how to structure the errors object to correspond to the values object.
  • You might want to create helper types or interfaces to improve the type safety of your hook.
  • For validation, the validation rule function for a field should return an empty string ('') if there are no errors for that field, and a string representing the error message otherwise.
Loading editor...
typescript