reactjsreact-hooks

Building a React Component that can be both Controlled and Uncontrolled


I am trying to get my head around the difference between controlled and uncontrolled react components, so thought i would have a go at building a control that can be either but not both. It seems like the pattern used by <input> is that if you provide a value prop then it will be controlled, otherwise uncontrolled and you can provide a default value to uncontrolled using defaultValue prop.

My example control is a simple number incrementer/decrementer with buttons to increment and decrement and a label showing the current value.

My questions are .

  1. Have I gone about this the right way.
  2. I have written a number of tests to cover all the scenarios i can think of, are these all valid and am i missing any.

I am hoping through this example and any feedback to get a thorough understanding of controlled vs uncontrolled, and when to use each.

My code and all the tests are available in this codesandbox https://codesandbox.io/s/kind-archimedes-cs0qy

but my component is repeated here for ease ...

    import React, { useState } from "react";

    export const NumberInput = ({ onChange, value, defaultValue, min, max }) => {
      const [uncontrolledVal, setUncontrolledVal] = useState(
        defaultValue || min || 0
      );

      if (
        (value && (value > max || value < min)) ||
        (defaultValue && (defaultValue > max || defaultValue < min))
      ) {
        throw new Error("Value out of range");
      }

      const handlePlusClick = () => {
        if (value && onChange) {
          onChange(value + 1);
        } else {
          const newValue = uncontrolledVal + 1;
          setUncontrolledVal(newValue);
          if (onChange) {
            onChange(newValue);
          }
        }
      };
      const handleMinusClick = () => {
        if (value && onChange) {
          onChange(value - 1);
        } else {
          const newValue = uncontrolledVal - 1;
          setUncontrolledVal(newValue);
          if (onChange) {
            onChange(newValue);
          }
        }
      };
      return (
        <>
          <button
            data-testid="decrement"
            disabled={value ? value === min : uncontrolledVal === min}
            onClick={() => handleMinusClick()}
          >
            {"-"}
          </button>
          <span className="mx-3 font-weight-bold">{value || uncontrolledVal}</span>
          <button
            data-testid="increment"
            disabled={value ? value === max : uncontrolledVal === max}
            onClick={() => handlePlusClick()}
          >
            {"+"}
          </button>
        </>
      );
    };

Solution

  • For something like this, a custom incrementer, there is no such thing as an uncontrolled version. Uncontrolled applies to the built in input fields in HTML 5, such as a text field, a checkbox, or a file upload.

    Controlled means the value of an input is set by a state or prop value and updated with a custom function. uncontrolled means it handles its own changes for its value and you have to manually retrieve their values when you want to use them.

    This can be accomplished using something like a ref.

    If you bind the value of an input field to a value without providing a function to update its value onChange, it will not respond to any user input. However you can provide a defaultValue to it, and it will still respond to user input.

    You can read more about it here: https://reactjs.org/docs/uncontrolled-components.html

    Does that answer your question?