Demystifying React Forms: Controlled, Uncontrolled Components and More

Demystifying React Forms: Controlled, Uncontrolled Components and More

·

6 min read

React offers a robust framework for building dynamic web applications, and understanding how to manage forms within React effectively can dramatically streamline your development process. In this blog, we'll dive into the distinctions between controlled and uncontrolled components and container and presentational components, using a simple recipe application as our guide.

React Forms: Controlled vs Uncontrolled Components

In React, form data handling can be approached in two distinct ways: using controlled or uncontrolled components. Each method serves different needs and simplifies form management in unique scenarios.

Controlled Components

Controlled components are React components that manage form data with state. Here, the form data is controlled by React, as shown in our AddRecipeForm component:

// AddRecipeForm component, focusing on controlled components
const AddRecipeForm = ({ onAddRecipe }) => {
  const [recipe, setRecipe] = useState({
    name: "",
    ingredients: "",
    instructions: "",
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setRecipe({ ...recipe, [name]: value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onAddRecipe(recipe);
    setRecipe({
      name: "",
      ingredients: "",
      instructions: "",
    });
  };

  return (
    <div className="container">
      <h2>Add a New Recipe</h2>
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <input
            type="text"
            id="name"
            name="name"
            value={recipe.name}
            onChange={handleChange}
          />
          {/* Other inputs */}
        </div>
        <button type="submit">Add Recipe</button>
      </form>
    </div>
  );
};

In this example, every form element is linked to the React state via the value attribute, making the React state the "single source of truth." This means the input’s state will be updated with each keystroke, reflecting the changes immediately.

Uncontrolled Components

Conversely, uncontrolled components use refs to get form values from the DOM, which means React does not manage the form data directly. Here, we would use useRef and attach the ref to our form inputs to retrieve their values when needed, typically at form submission. This approach is closer to traditional HTML form handling.

import React, { useRef } from 'react';

const UncontrolledRecipeForm = ({ onAddRecipe }) => {
    // Create refs for each input field using useRef
    const nameInput = useRef(null);
    const ingredientsInput = useRef(null);
    const instructionsInput = useRef(null);

    const handleSubmit = (event) => {
        // Prevent the default form submit behavior
        event.preventDefault();

        // Construct a recipe object from the values of the refs
        const newRecipe = {
            name: nameInput.current.value,
            ingredients: ingredientsInput.current.value,
            instructions: instructionsInput.current.value,
        };

        // Pass the new recipe up to the parent component
        onAddRecipe(newRecipe);

        // Reset the form fields
        nameInput.current.value = '';
        ingredientsInput.current.value = '';
        instructionsInput.current.value = '';
    };

    return (
        <div className="container">
            <h2>Add a New Recipe</h2>
            <form onSubmit={handleSubmit}>
                <div className="form-group">
                    <label htmlFor="name">Recipe Name</label>
                    <input
                        type="text"
                        id="name"
                        ref={nameInput}
                    />
                </div>
                <div className="form-group">
                    <label htmlFor="ingredients">Ingredients</label>
                    <input
                        type="text"
                        id="ingredients"
                        ref={ingredientsInput}
                    />
                </div>
                <div className="form-group">
                    <label htmlFor="instructions">Instructions</label>
                    <textarea
                        id="instructions"
                        ref={instructionsInput}
                    />
                </div>
                <button type="submit">Add Recipe</button>
            </form>
        </div>
    );
};

export default UncontrolledRecipeForm;

Container vs Presentational Components

Understanding the roles of container and presentational components can significantly tidy up your React architecture.

Container Components

Container components are concerned with how things work. They provide data and behavior to other components. In our app, the main App component acts as a container:

// App component, focusing on container responsibilities
function App() {
  const [recipes, setRecipes] = useState([...]); // Initial recipes

  const addRecipe = (newRecipe) => {
    setRecipes([...recipes, { ...newRecipe, id: recipes.length + 1 }]);
  };

  return (
    <div className="App">
      <RecipeList recipes={recipes} onRecipeSelect={handleSelect} />
      <AddRecipeForm onAddRecipe={addRecipe} />
    </div>
  );
}

This component manages the state and functions needed to modify it, passing down props to the child components.

Presentational Components

Presentational components, by contrast, focus on how things look. They receive data and callbacks exclusively via props. Consider our RecipeList component:

// RecipeList component, a typical presentational component
const RecipeList = ({ recipes, onRecipeSelect }) => {
  return (
    <div className="recipe-container">
      {recipes.map((recipe) => (
        <div key={recipe.id} className="recipe-card" onClick={() => onRecipeSelect(recipe)}>
          {/* Recipe details */}
        </div>
      ))}
    </div>
  );
};

This component is solely responsible for the UI representation, displaying the list of recipes without managing any state itself.

When to Use Controlled vs. Uncontrolled Components

Controlled Components

Use when:

  • You need fine-grained control over form data at every stage of the input lifecycle, such as when implementing complex validation logic or dynamically enabling/disabling submit buttons based on the form’s validity.

  • Instantaneous feedback to the user is required, such as input format validation or live previews of the intended input.

  • State synchronization is necessary, where input values need to be synced with other UI elements in real-time, ensuring consistent state across multiple parts of the application.

Example Scenario: A registration form where fields must be validated immediately to provide feedback on password strength or the format of an email address before allowing the user to proceed.

Uncontrolled Components

Use when:

  • Less complexity is required in the form and you wish to avoid managing form data with state. This is often suitable for simpler forms like a quick contact or feedback form.

  • You want to integrate with non-React code, such as third-party DOM-based libraries, where direct manipulation of the DOM is easier or necessary.

  • Performance optimization is crucial, especially in very large forms where individual updates to the state might cause performance issues.

Example Scenario: A simple newsletter subscription form where the user enters an email address without any need for immediate validation or feedback.

Deciding Between Container and Presentational Components

Container Components

Use when:

  • Data fetching or state management is involved. If the component needs to handle logic, connect to external data sources, or manage significant parts of the application state, it should be a container component.

  • Propagating data to children is required. If a component acts as a data layer to other nested components, organizing it as a container can help isolate complex data management logic from UI rendering.

Example Scenario: An app component that fetches user data from a server and passes it down to various UI components, handling user authentication states and directing the flow of data.

Presentational Components

Use when:

  • Focus on the UI. When the primary role of a component is to present content or render UI elements based on passed props, without the involvement of data fetching or handling application logic.

  • Reusability is a key factor. Presentational components can be easily reused across different parts of the application with different data inputs.

Example Scenario: A Button component that accepts props like onClick, children, and style but does not interact directly with data sources or manage state beyond its direct UI concerns.

Incorporating These Decisions into Your Application

Incorporating the decision-making process on whether to use controlled or uncontrolled components, or whether to design a component as container or presentational, is crucial for developing maintainable and scalable React applications. Understanding these distinctions and using them effectively can significantly enhance your development workflow and application performance. By aligning the component's purpose with its design, you ensure that your React application remains organized and efficient, making it easier to manage, test, and scale.

Wrapping Up

Understanding the distinctions between controlled vs uncontrolled components and container vs. presentational components allows developers to structure their React applications more effectively. The AddRecipeForm and RecipeList Our recipe app illustrates these concepts practically and easily understandable, showing how React’s component architecture can be leveraged to build scalable and maintainable applications. By mastering these patterns, you can ensure that your React forms and components are robust and efficient.