Hone logo
Hone
Problems

Implement useFormContext Hook in React with TypeScript

Forms are a fundamental part of most web applications. Managing form state, validation, and submission can become complex, especially in larger applications. React's Context API offers a powerful way to share data across components without prop drilling. This challenge asks you to create a useFormContext hook that leverages React Context to provide form state and manipulation functions to descendant components.

Problem Description

Your task is to implement a useFormContext hook in React using TypeScript. This hook should be designed to work in conjunction with a FormContext provider. The primary goal is to simplify form management by allowing any component within the provider's tree to access and modify form data, as well as trigger form submission.

Key Requirements:

  1. FormContext Provider: You'll need to define a FormContext and a corresponding FormProvider component. The FormProvider will hold the form's state and logic.
  2. useFormContext Hook: Implement a hook named useFormContext. This hook should:
    • Access the form context provided by FormProvider.
    • Return an object containing the current form values, a function to update individual form values, and a function to handle form submission.
    • If useFormContext is called outside of a FormProvider, it should throw a descriptive error.
  3. Form State Management: The FormProvider should manage the form's state (values) internally.
  4. Value Updates: The FormProvider should provide a function that allows updating specific fields within the form's state.
  5. Form Submission: The FormProvider should provide a function to trigger a form submission. This function should receive the current form values as an argument.
  6. TypeScript Integration: Ensure all types are strictly defined using TypeScript.

Expected Behavior:

  • Components consuming useFormContext should receive the latest form values.
  • Calling the update function from a child component should re-render only the necessary parts of the application and update the form state correctly.
  • Calling the submit function should execute the provided submit handler with the current form values.

Edge Cases to Consider:

  • What happens if useFormContext is used without a FormProvider ancestor?
  • How should deeply nested components access the form context?

Examples

Let's assume a hypothetical FormContext and FormProvider are already set up. You are focusing on the useFormContext hook and how it interacts with the context.

Example 1: Basic Usage

Scenario: A form with a single text input.

// Assume this is provided by your FormProvider setup
// interface FormContextType {
//   values: { [key: string]: any };
//   updateValue: (field: string, value: any) => void;
//   handleSubmit: () => void;
// }
// const FormContext = React.createContext<FormContextType | undefined>(undefined);

// Your implementation of useFormContext would go here:
function useFormContext<T extends object>() {
  const context = React.useContext(FormContext as React.Context<FormContextType<T> | undefined>);
  if (context === undefined) {
    throw new Error('useFormContext must be used within a FormProvider');
  }
  return context;
}

// Example Usage in a form component:
function MyForm() {
  // Assume FormProvider wraps this component
  const { values, updateValue, handleSubmit } = useFormContext<{ name: string }>();

  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    updateValue('name', e.target.value);
  };

  const onSubmit = () => {
    console.log('Form submitted with:', values);
    // Typically would involve an API call
  };

  // In a real scenario, FormProvider would likely take an onSubmit prop
  // and call it internally when handleSubmit is triggered.
  // For this example, we'll simulate it:
  const simulatedHandleSubmit = () => {
    onSubmit();
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); simulatedHandleSubmit(); }}>
      <label>
        Name:
        <input
          type="text"
          value={values.name || ''}
          onChange={handleNameChange}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

Input to useFormContext: N/A (it consumes context)

Output of useFormContext (within MyForm): An object like { values: { name: 'Initial Name' }, updateValue: [function], handleSubmit: [function] }.

Explanation: The useFormContext hook successfully retrieves the form state and functions from the context, allowing the MyForm component to display the current name value and update it via updateValue. The handleSubmit function is also available for form submission.

Example 2: Updating Nested Values (Conceptual)

Scenario: A form with a nested object for user profile information.

// Assume FormProvider context setup as before

function ProfileForm() {
  const { values, updateValue } = useFormContext<{ profile: { firstName: string; lastName: string } }>();

  const handleFirstNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // updateValue needs to handle nested updates gracefully
    updateValue('profile.firstName', e.target.value);
  };

  const handleLastNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    updateValue('profile.lastName', e.target.value);
  };

  return (
    <div>
      <label>
        First Name:
        <input
          type="text"
          value={values.profile?.firstName || ''}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last Name:
        <input
          type="text"
          value={values.profile?.lastName || ''}
          onChange={handleLastNameChange}
        />
      </label>
    </div>
  );
}

Input to useFormContext: N/A

Output of useFormContext (within ProfileForm): An object like { values: { profile: { firstName: 'John', lastName: 'Doe' } }, updateValue: [function], handleSubmit: [function] }.

Explanation: The updateValue function, when called with a dot-notation string like 'profile.firstName', should correctly update the nested property within the values object.

Example 3: Error Handling

Scenario: Using useFormContext outside of a FormProvider.

// Assume FormProvider context setup as before

function OutsideComponent() {
  // This component is NOT wrapped by FormProvider
  try {
    const { values } = useFormContext();
    return <div>Form Values: {JSON.stringify(values)}</div>;
  } catch (error) {
    return <div>Error: {error.message}</div>;
  }
}

Input to useFormContext: N/A

Output of useFormContext (within OutsideComponent): The catch block will execute, and the component will render: Error: useFormContext must be used within a FormProvider.

Explanation: Because OutsideComponent does not have a FormProvider ancestor, React.useContext will return undefined. The useFormContext hook correctly checks for this undefined value and throws a descriptive error.

Constraints

  • The useFormContext hook should be implemented in pure TypeScript.
  • The solution should aim for efficient re-renders, meaning updates to form values should ideally only cause re-renders of components that specifically consume those values.
  • The FormProvider should accept an initialValues prop of type T.
  • The FormProvider should accept an onSubmit prop which is a function that receives the form values of type T.

Notes

  • Consider how you will represent the form's values. A plain JavaScript object is a good starting point.
  • Think about how to handle updates to nested object properties within the form values. A helper function might be useful.
  • The handleSubmit function provided by the context should internally call the onSubmit prop passed to the FormProvider.
  • You are free to structure your FormContextType and FormProvider as you see fit, as long as they enable the useFormContext hook to function as described. The core challenge is the useFormContext hook itself.
Loading editor...
typescript