DEV Community

Dharani Jayakanthan - Danny
Dharani Jayakanthan - Danny

Posted on

Form Builder Using Flexible Compound Component in React

In this blog, we’ll explore how to build a form builder that updates the parent component as the user interacts with the form. This approach can be particularly useful in real-time validation, autosaving, or scenarios where you want to keep the parent state in sync with form inputs immediately as they change.

We will be leveraging Flexible Compound Component pattern for this form builder.

What is Flexible Compound Component

  • Compound Components are patterns used to create flexible and reusable components where the parent component can control the behavior and render children dynamically.
  • Compound components allow developers to separate logic and presentation, leading to cleaner, more maintainable code.
  • This pattern promotes customization without compromising on the separation of concerns
  • With flexible compound components, you can allow end-users of your component to configure their own behavior while keeping the core logic intact.

How the Compound Component Pattern Works in React

  • The parent component serves as the core of the compound component, managing the state and providing methods or handlers that the child components can utilize. It passes data to its children either directly as props or, more commonly, through React’s Context API.
  • Child components within the compound component structure are responsible for rendering individual pieces of the UI. They consume the state and behavior that the parent component provides, allowing for modularity and separation of concerns.
  • The power of the compound component pattern lies in the composition of multiple child components within the parent component. You can add as many child components as necessary, and they will automatically work together due to the shared context and state from the parent.

Building a Form Builder using Flexible Compound Component: Step-by-Step

With the Flexible Compound Component Design Pattern, we can create a form that directly updates the parent state in real-time.

  • The parent component holds the form state.
  • Child components like Input, Checkbox, or Dropdown receive state from the parent and notify the parent when they change.
  • Form field directly informs the parent of any change.

Let’s create a controlled form that updates the parent state as soon as any input field changes.


1. Setting Up the Parent Form Component

The parent Form component will manage the state and provide the state and onChange handler to the child components via context. Each input field will update the form state in real-time.

import React, { useContext, useState } from 'react';
import { FormContext } from './context/formContext';

export const Form = ({ children, onFormChange }) => {
  const [formData, setFormData] = useState({});

  const updateField = (name, value) => {
    const updatedData = {
      ...formData,
      [name]: value,
    };
    setFormData(updatedData);
    onFormChange(updatedData); // Notify parent about the form change immediately
  };

  return (
    <FormContext.Provider value={{ formData, updateField }}>
      <div>{children}</div> {/* No <form> tag, direct div usage */}
    </FormContext.Provider>
  );
};

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The Form component manages formData internally.
  • The updateField function updates the state for the individual fields and immediately informs the parent of changes via onFormChange.
  • This structure updates the parent directly.

2. Creating the FormInput Component

The FormInput component will use the context to retrieve the form’s current state and update the value when the user types.

export function FormInput({ name, label, type = 'text' }) {
  const { formData, updateField } = useContext(FormContext);

  const handleInputChange = (e) => {
    updateField(name, e.target.value);
  };

  return (
    <div>
      <label>{label}</label>
      <input
        type={type}
        name={name}
        value={formData[name] || ''}
        onChange={handleInputChange}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • FormInput uses the FormContext to access the form’s state and updateField handler.
  • It immediately updates the field’s value and calls the parent’s onFormChange function through context as the user types.

3. Creating a FormCheckbox Component

We can also create a checkbox component to showcase how to manage different form elements.

export function FormCheckbox({ name, label }) {
  const { formData, updateField } = useContext(FormContext);

  const handleCheckboxChange = (e) => {
    updateField(name, e.target.checked);
  };

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={formData[name] || false}
          onChange={handleCheckboxChange}
        />
        {label}
      </label>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The checkbox works similarly to the FormInput but toggles between true and false based on the checked property.
  • Again, it informs the parent about the state change instantly via the context.

4. Using the Form Components Together

Now let’s see how to use the form components in the parent component and handle the form changes.

function App() {
  const [formState, setFormState] = useState({});

  const handleFormChange = (updatedData) => {
    setFormState(updatedData);
    console.log('Updated form data:', updatedData);
  };

  return (
    <div>
      <h1>Controlled Form Builder</h1>
      <Form onFormChange={handleFormChange}>
        <FormInput name="firstName" label="First Name" />
        <FormInput name="lastName" label="Last Name" />
        <FormInput name="email" label="Email" type="email" />
        <FormCheckbox name="acceptTerms" label="Accept Terms" />
      </Form>
      <pre>{JSON.stringify(formState, null, 2)}</pre> {/* Display current form state */}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example:

  • App component holds the formState.
  • The handleFormChange function is passed to the Form component and updates the formState every time any input changes.

Extending the Form Builder

Now that we have a basic controlled form builder that updates the parent state on every change, let’s extend it by adding dynamic fields and validation in coming blogs.


Conclusion

The Flexible Compound Component Design Pattern in React allows you to build modular, controlled form components that can update their parent component in real-time. This pattern:

  • Easy styling complex form UI's.
  • Addition of any components in between the Form Component.
  • Keeps the parent component in sync with the form state.
  • Is flexible enough to handle dynamic fields, validation, and various input types.

This approach is ideal for scenarios where you need real-time form updates, like form builders, live-editing forms, and autosave applications. It offers a clean and maintainable structure that scales well with complexity.


Demo

Happy building!

Top comments (0)