DEV Community

Cover image for React: Design Patterns | Controlled & Uncontrolled Components
Andres Z
Andres Z

Posted on

React: Design Patterns | Controlled & Uncontrolled Components

React is a powerful library for building user interfaces, and one of its core strengths lies in its flexibility. Among the many design patterns React offers, controlled and uncontrolled components are fundamental. Understanding these patterns can significantly impact how you manage state and handle user input in your applications.

Uncontrolled Components

Uncontrolled components are those where the component itself maintains its own internal state. The only time you interact with this state is when an event occurs, such as a form submission. For instance, consider a form where you only access the input values when the user hits the submit button. Until that event, the form's state is entirely managed by the DOM elements themselves.

Here's a simple example of an uncontrolled form:

import React, { createRef } from 'react';

export const UncontrolledForm = () => {
  const nameInput = createRef();
  const ageInput = createRef();
  const hairColorInput = createRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(nameInput.current.value);
    console.log(ageInput.current.value);
    console.log(hairColorInput.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" type="text" placeholder="Name" ref={nameInput} />
      <input name="age" type="number" placeholder="Age" ref={ageInput} />
      <input name="hairColor" type="text" placeholder="Hair Color" ref={hairColorInput} />
      <input type="submit" value="Submit" />
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, the form's state is only accessed when the form is submitted. The createRef function is used to create references to the input elements, which are then accessed in the handleSubmit function.

Controlled Components

Controlled components, on the other hand, rely on their parent component to manage their state. The state is passed down as props, and any changes to the state are handled by callback functions provided by the parent.

Here's how you might implement a controlled form

import React, { useState } from 'react';

export const ControlledForm = () => {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  const [hairColor, setHairColor] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(name, age, hairColor);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        type="text"
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        name="age"
        type="number"
        placeholder="Age"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
      />
      <input
        name="hairColor"
        type="text"
        placeholder="Hair Color"
        value={hairColor}
        onChange={(e) => setHairColor(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this controlled form, the state of each input is managed by React's useState hook. The value prop of each input is tied to its corresponding state variable, and the onChange handler updates the state whenever the user types into the input.

Why Prefer Controlled Components?

Controlled components are generally preferred for several reasons:

  1. Reusability: Controlled components are more reusable because their behavior is determined by their props rather than their internal state.
  2. Testability: They are easier to test since you can set up a component with a specific state and verify its behavior without needing to simulate user interactions.
  3. Predictability: With controlled components, you have a single source of truth for your state, making your application more predictable and easier to debug.

Practical Examples

Uncontrolled Forms

Uncontrolled forms defer most of their logic to the DOM elements. Here's an example:

import React, { createRef } from 'react';

export const UncontrolledForm = () => {
  const nameInput = createRef();
  const ageInput = createRef();
  const hairColorInput = createRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(nameInput.current.value);
    console.log(ageInput.current.value);
    console.log(hairColorInput.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" type="text" placeholder="Name" ref={nameInput} />
      <input name="age" type="number" placeholder="Age" ref={ageInput} />
      <input name="hairColor" type="text" placeholder="Hair Color" ref={hairColorInput} />
      <input type="submit" value="Submit" />
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Controlled Forms

Controlled forms manage their state through React's useState hook:

import React, { useState } from 'react';

export const ControlledForm = () => {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  const [hairColor, setHairColor] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(name, age, hairColor);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        type="text"
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        name="age"
        type="number"
        placeholder="Age"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
      />
      <input
        name="hairColor"
        type="text"
        placeholder="Hair Color"
        value={hairColor}
        onChange={(e) => setHairColor(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Controlled Modals

Modals can also be controlled or uncontrolled. An uncontrolled modal manages its own visibility state:

import React, { useState } from 'react';

export const UncontrolledModal = () => {
  const [shouldShow, setShouldShow] = useState(false);

  return (
    <>
      <button onClick={() => setShouldShow(true)}>Show Modal</button>
      {shouldShow && (
        <div className="modal">
          <p>Modal Content</p>
          <button onClick={() => setShouldShow(false)}>Close</button>
        </div>
      )}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

A controlled modal relies on its parent component to manage its visibility:

import React from 'react';

export const ControlledModal = ({ shouldShow, onRequestClose }) => {
  if (!shouldShow) return null;

  return (
    <div className="modal">
      <p>Modal Content</p>
      <button onClick={onRequestClose}>Close</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In the parent component:

import React, { useState } from 'react';
import { ControlledModal } from './ControlledModal';

const App = () => {
  const [shouldShowModal, setShouldShowModal] = useState(false);

  return (
    <>
      <button onClick={() => setShouldShowModal(!shouldShowModal)}>
        {shouldShowModal ? 'Hide Modal' : 'Show Modal'}
      </button>
      <ControlledModal shouldShow={shouldShowModal} onRequestClose={() => setShouldShowModal(false)} />
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Controlled and uncontrolled components each have their place in React development. While uncontrolled components can be simpler and quicker to implement for basic use cases, controlled components offer greater flexibility, reusability, and testability. Understanding when and how to use each pattern will make you a more effective React developer.

Top comments (0)