Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
        </>
      );
    };
like image 840
Mingo Avatar asked Dec 07 '25 10:12

Mingo


1 Answers

You can use useControllableState hook of @chakra-ui/react package

that allows any component handle controlled and uncontrolled modes, and provide control over its internal state

You can find the counter usage

With useControllableState, you can pass an initial state (using defaultValue) implying the component is uncontrolled, or you can pass a controlled value (using value) implying the component is controlled.

So the props of your <NumberInput/> component designed correctly.

Now you can use useControllableState to make your component handle controlled and uncontrolled modes:

import React from "react";
import { useControllableState } from "@chakra-ui/react";

export const NumberInput = ({ onChange, value, defaultValue, min, max }) => {
  const [state, setState] = useControllableState({
    defaultValue: defaultValue || min || 0,
    value,
    onChange,
  });

  const handlePlusClick = () => {
    setState(state + 1);
  };
  const handleMinusClick = () => {
    setState(state - 1);
  };
  return (
    <>
      <button data-testid="decrement" onClick={() => handleMinusClick()}>
        {"-"}
      </button>
      <span className="mx-3 font-weight-bold">{state}</span>
      <button data-testid="increment" onClick={() => handlePlusClick()}>
        {"+"}
      </button>
    </>
  );
};

Live demo

In your example, there is a use case which <NumberInput/> component doesn't have the onChange prop. The official docs explain this pitfall:

If you pass value without onChange, it will be impossible to type into the input. When you control an input by passing some value to it, you force it to always have the value you passed. So if you pass a state variable as a value but forget to update that state variable synchronously during the onChange event handler, React will revert the input after every keystroke back to the value that you specified.

like image 65
slideshowp2 Avatar answered Dec 10 '25 01:12

slideshowp2