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 .
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>
</>
);
};
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 (usingdefaultValue) implying the component is uncontrolled, or you can pass a controlled value (usingvalue) 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
valuewithoutonChange, 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 thevalueyou passed. So if you pass a state variable as avaluebut forget to update that state variable synchronously during theonChangeevent handler, React will revert the input after every keystroke back to the value that you specified.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With