How to use React Hook Form with multiple FormProvider contexts?

When using react-hook-form and useFormContext with multple forms inside the provider, on submit, how do I get each useFormContext data part as a separate object?

While useFormContext in react-hook-form allows accessing form context within a FormProvider, it doesn’t directly solve submitting multiple forms with separate data objects. Here are two common approaches to achieve this:

1. Wrapping each form with its own FormProvider:

This approach uses a dedicated FormProvider and useForm for each individual form. Upon submission, each form’s handleSubmit function returns the form data as a separate object.

Here’s an example:

JavaScript
import { Form, FormProvider, useForm, useFormContext } from "react-hook-form";

const Form1 = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log("Form 1 data:", data); // This is a separate object
  };

  return (
    <FormProvider {...register}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input type="text" {...register("name", { required: true })} />
        {errors.name && <span>Name is required</span>}
        <button type="submit">Submit Form 1</button>
      </form>
    </FormProvider>
  );
};

const Form2 = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log("Form 2 data:", data); // This is another separate object
  };

  return (
    <FormProvider {...register}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input type="email" {...register("email", { required: true })} />
        {errors.email && <span>Email is required</span>}
        <button type="submit">Submit Form 2</button>
      </form>
    </FormProvider>
  );
};

const App = () => {
  return (
    <div>
      <Form1 />
      <Form2 />
    </div>
  );
};

export default App;

In this example, Form1 and Form2 each handle their own data through useForm and submit separate objects on submission.

2. Using a custom hook to manage multiple forms:

This approach creates a custom hook to manage multiple forms with a single FormProvider. The hook uses an internal state to store form data for each form and exposes functions to access and submit data.

Here’s a simplified example:

JavaScript
import { Form, useForm } from "react-hook-form";

const useMultiForm = () => {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  const [formData, setFormData] = React.useState({ form1: {}, form2: {} });

  const handleForm1Submit = (data) => {
    setFormData({ ...formData, form1: data });
    // You can also perform further actions with data here
  };

  const handleForm2Submit = (data) => {
    setFormData({ ...formData, form2: data });
    // You can also perform further actions with data here
  };

  return {
    register,
    handleSubmit,
    watch,
    errors,
    formData,
    handleForm1Submit,
    handleForm2Submit,
  };
};

const App = () => {
  const { register, handleSubmit, watch, errors, formData, handleForm1Submit, handleForm2Submit } = useMultiForm();

  return (
    <div>
      <Form onSubmit={handleSubmit(handleForm1Submit)}>
        {/* Form 1 fields with register */}
      </Form>
      <Form onSubmit={handleSubmit(handleForm2Submit)}>
        {/* Form 2 fields with register */}
      </Form>
      <pre>{JSON.stringify(formData, null, 2)}</pre>
    </div>
  );
};

export default App;

This approach maintains a single FormProvider while managing data and submissions within the custom hook. You can customize it further to fit your specific needs, such as handling form validation and error handling.

Both approaches handle submitting multiple forms with separate data objects. Choose the one that best suits your application’s structure and complexity.

It seems this can be solved by prefixing the key used for inputs. Please show an example.

You’re absolutely right! You can indeed utilize key prefixes for your inputs to achieve separate data objects in each form within a single FormProvider. Here’s an example:

JavaScript
import { Form, FormProvider, useForm } from "react-hook-form";

const App = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log("Combined   
 form data:", data);
  };

  return (
    <FormProvider {...register}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <Form.Group>
          <h2>Form 1</h2>
          <input 
            type="text" 
            {...register("form1.name", { required: true })} 
            placeholder="Name"
          />
          {errors["form1.name"] && <span>Name is required</span>}
          <input 
            type="email" 
            {...register("form1.email", { required: true })} 
            placeholder="Email"
          />
          {errors["form1.email"] && <span>Email is required</span>}
        </Form.Group>
        <Form.Group>
          <h2>Form 2</h2>
          <input 
            type="text" 
            {...register("form2.phone", { required: true })} 
            placeholder="Phone Number"
          />
          {errors["form2.phone"] && <span>Phone Number is required</span>}
          <input 
            type="text" 
            {...register("form2.address", { required: true })} 
            placeholder="Address"
          />
          {errors["form2.address"] && <span>Address is required</span>}
        </Form.Group>
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default App;

In this example:

  1. We use a single FormProvider and useForm for both forms.
  2. We define our input names with a prefix that corresponds to the form (e.g., form1.name, form2.phone).
  3. The useForm hook will automatically combine all registered values into a single data object upon submission, where each key has the prefixed form name and field name.

This approach provides a concise way to handle separate form data while utilizing a single provider. Remember to adjust the field names and prefixes based on your specific form structure.

Is there a way to auto-prefix so I do not need to write it each time?

Unfortunately, react-hook-form doesn’t offer built-in functionality for automatic prefixing. However, you can achieve it using a custom wrapper component or higher-order component (HOC). Here’s an example using a wrapper component:

JavaScript
import { Form, useForm } from "react-hook-form";

const FormWrapper = ({ children, formName }) => {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const handleFormSubmit = (data) => {
    console.log(`${formName} data:`, data);
  };

  return (
    <Form onSubmit={handleSubmit(handleFormSubmit)}>
      {children.map((child) => {
        return React.cloneElement(child, {
          ...child.props,
          {...register(`${formName}.${child.props.name}`, child.props)},
        });
      })}
      {errors &&
        Object.entries(errors).map(([key, value]) => (
          <span key={key}>{value.message}</span>
        ))}
      <button type="submit">Submit</button>
    </Form>
  );
};

const App = () => {
  return (
    <div>
      <FormWrapper formName="form1">
        <input type="text" name="name" placeholder="Name" />
        <input type="email" name="email" placeholder="Email" />
      </FormWrapper>
      <FormWrapper formName="form2">
        <input type="text" name="phone" placeholder="Phone Number" />
        <input type="text" name="address" placeholder="Address" />
      </FormWrapper>
    </div>
  );
};

export default App;

This approach:

  1. Defines a FormWrapper component that takes children and formName as props.
  2. Uses useForm within the wrapper and defines handleFormSubmit to log data with the provided formName.
  3. Iterates through children and clones each element, adding the register prop with the prefixed name (formName.child.props.name).
  4. Renders any form errors based on the combined errors object.

This way, you can wrap your forms with the FormWrapper, providing the desired form name, and it automatically prefixes the input names with the form name. Remember to adjust this example to match your specific component structure and error handling needs.

React.js Context with setter – Where to create a new reference?

Modern React.js is prone to too many re-renders if done wrong.
One part that is of a higher risk, are contexts.
The reason for this is how they cause re-renders in children of the context provider.

The re-render is typically done due to a state change via useState or useReducer.
As long as this is done properly, this should not be an issue.
However, in more complex contexts, the data could change reference at some levels but not at others.
There is also the issue of setters in contexts. If a setter is used, should the whole object change reference? The function too? Just the main data reference?

Just to make sure, I asked Bard AI to get some inspiration:


Does a react.js reducer need to return a new object each time?


In React with Redux, reducers ideally should return a new object every time, even if there’s no state change. Here’s why:

  • Immutability: This is a core principle in Redux. By returning a new object, you avoid accidentally modifying the existing state. This makes reasoning about state updates easier and prevents unintended side effects.

  • Performance Optimization: Redux relies on shallow comparison to detect state changes. Returning a new object, even an identical one, forces a re-render of components that rely on that state. This can trigger optimizations in React’s virtual DOM, potentially improving performance.

There might be cases where the reducer can return the original state if there’s no change. However, it’s generally considered a best practice to always return a new object. This ensures consistency and avoids potential bugs.

Here are some ways to achieve this:

  • Spread Operator (...): This is a common approach. You can copy the existing state and modify specific properties within the new object.

  • Immutable.js Library: This library provides helper functions for creating and manipulating immutable data structures.

Remember, even if the reducer doesn’t modify the state, returning a new object can still benefit readability and maintainability of your code.


However, the reference to the context changes each time, and I am using the reference in a useCallback function, for example: context.setData, what should I do?


You’re right, there’s a potential issue when using useCallback with a context that changes on every render. Here’s how to address it:

The Problem:

useCallback memorizes a function based on its reference. If the context containing setData changes on every render, useCallback will create a new function reference each time. This defeats the purpose of useCallback as child components will receive a new function on every render, even if the logic within the function hasn’t changed.

Solutions:

  1. Prevent Context from Changing Unnecessarily:

    • Check if your context truly needs to change on every render. If it holds static data or only updates occasionally, consider refactoring it to update less frequently.
  2. Extract setData Outside of useCallback:

    • Move the logic for getting setData from the context outside of the useCallback function. This way, you only capture the reference to the function itself, not the context that might change.

    Here’s an example:

    JavaScript
    const MyContext = React.createContext();
    
    const MyComponent = () => {
      const context = useContext(MyContext);
    
      // Extract setData outside of useCallback
      const setData = context.setData;
    
      const memoizedFunction = useCallback(() => {
        // Use setData here
        setData(/* your arguments */);
      }, [/* no dependencies */]);
    
      return (
        {/* ... */}
      );
    };
    
  3. Combine useCallback with useMemo:

    • In some cases, you might need to keep the setData reference within the useCallback function. Here, you can combine useCallback with useMemo to create a memoized function that references the current setData function from the context.
    JavaScript
    const memoizedFunction = useCallback(() => {
      const context = useContext(MyContext);
      const setData = context.setData;
    
      return () => {
        // Use setData here
        setData(/* your arguments */);
      }
    }, [context]); // useCallback depends on context changes
    

Choosing the Right Approach:

The best solution depends on your specific use case. If you can prevent unnecessary context updates, that’s ideal. Otherwise, consider extracting setData or using the combination of useCallback and useMemo.


Should the context reference change on every data change? Even if for example I have context data of { data, setData } where only data is the data to be stored and referenced?


No, the context reference ideally shouldn’t change on every data change, especially in the scenario you describe where data is the primary data store and setData is the update function. Here’s why:

  • Unnecessary Re-renders: If the context reference changes with every data update, components that rely on useContext will re-render even if the data itself hasn’t changed. This can lead to performance issues, especially for deeply nested components.

  • useCallback Issues: As you mentioned previously, useCallback relies on reference equality. Changing the context reference with every update will defeat the purpose of useCallback in child components.

Better Practices for Context Updates:

Here are some approaches to keep the context reference stable while updating data:

  1. Immutable Updates:

    • Update the data property within the context by creating a new object with the updated value. You can use the spread operator (...) to achieve this:
    JavaScript
    const MyContext = React.createContext({ data: [], setData: () => {} });
    
    const updateData = (newData) => {
      dispatch({ type: 'UPDATE_DATA', data: newData });
    };
    
    const reducer = (state, action) => {
      switch (action.type) {
        case 'UPDATE_DATA':
          return { ...state, data: action.data };
        default:
          return state;
      }
    };
    
  2. Context with Reducer:

    • If your data updates are complex, consider using a reducer with your context. This allows you to manage state updates in a more predictable way and keeps the context reference stable.
  3. useMemo for Selective Updates:

    • In some cases, you might only need to re-render specific parts of your UI based on data changes. Here, you can use useMemo to create a memoized value derived from the context data. This memoized value will only update when its dependencies (specific parts of the data) change, preventing unnecessary re-renders.

By following these practices, you can ensure that your context reference remains stable while effectively managing data updates in your React application.


As can be seen by this dialogue:

  • The value returned from contexts does not need to change.
  • The setter function does not need to change.
  • Any reducers used should return a new reference.
  • Therefore, the data reference should be expected to change on update and so can be watched for changes.