skip to content
Samuel Edwin's Website

Build a Controlled and Uncontrolled Component with React

/ 4 min read

In the previous post we have learned about controlled and uncontrolled component, and also the difference between those two.

In this post we’re going to build a component that supports both controlled and uncontrolled mode from scratch.

Let’s get started by building a 5-star rating component.

Here’s what we’re going to do:

  1. We build the controlled and uncontrolled version of Rating component separately.
  2. Then we combine them into one component that support controlled and uncontrolled mode.

Controlled Rating component

Building a component in controlled mode should be familiar to those who have been learning React for a couple weeks.

Rating.js
export default function ControlledRating({
value,
onChange
}) {
// generate an array of 5 items
const items = Array(5).fill('');
return (
<span>
{items.map((_, index) => {
const isActive = value >= index + 1;
return (
<StarIcon
key={index}
isActive={isActive}
onClick={() => {
onChange?.(index + 1);
}}
/>
);
})}
</span>
);
}

We’re using the good old React props, nothing fancy about this code.

Here’s how we can use this component from our app.

The app is responsible for managing the displayed rating.

App.js
function App() {
const [rating, setRating] = useState(0)
return (
<ControlledRating value={rating}>
)
}

Uncontrolled Rating component

Let’s see how we build this uncontrolled Rating component.

UncontrolledRating.js
export default function UncontrolledRating({
defaultValue,
onChange
}) {
const [rating, setRating] = useState(defaultValue)
const items = Array(5).fill('');
return (
<span>
{items.map((_, index) => {
const isActive = rating >= index + 1;
return (
<StarIcon
key={index}
isActive={isActive}
onClick={() => {
onChange?.(index + 1);
setRating(index + 1);
}}
/>
);
})}
</span>
);
}

Here’s how it’s different compared to the previous controlled implementation:

  • We maintain the rating value of the component using an internal state.
  • When the star is clicked, we set the rating state to the desired value.

This is how we use the uncontrolled component.

App.js
function App() {
// store the value in a ref to prevent unnecessary re-renders
const ratingRef = useRef()
return (
<UncontrolledRating
value={rating}
onChange={rating => ratingRef.current = rating}
>
)
}

Combining them together

Now let’s combine those two components above into a single Rating component that supports both controlled and uncontrolled mode.

I will explain everything in the code comments so you can get better context.

Rating.js
export default function Rating({
value,
defaultValue,
onChange
}) {
// when value is defined, it means this component is in controlled mode
// and when defaultValue is defined, it means this component is uncontrolled.
// this is how we decide whether this component is controlled or not,
// so value and defaultValue must not be defined at the same time
if (value !== undefined && defaultValue !== undefined) {
throw new Error('value and defaultValue cannot exist at the same time');
}
// this is only used in uncontrolled mode
const [rating, setRating] = useState(defaultValue);
// the current rating comes from `value` when it is controlled
// or from the `rating` state when it is uncontrolled
const currentValue = value ?? rating ?? 0;
const items = Array(5).fill('');
return (
<span>
{items.map((_, index) => {
const isActive = currentValue >= index + 1;
return (
<button
onClick={() => {
onChange?.(index + 1);
setRating(index + 1);
}}
>
<StarIcon
key={index}
isActive={isActive}
/>
</button>
);
})}
</span>
);
}

And now we can use the component in both controlled and uncontrolled mode at the same time.

App.js
function App() {
// set controlled mode default value to 3
const [rating, setRating] = useState(3)
const ratingRef = useRef()
return (
<div>
{/* controlled mode */}
<Rating
value={rating}
onChange={rating => setRating(rating)} />
{/* uncontrolled mode */}
<Rating
defaultValue={3}
onChange={rating => ratingRef.current = rating}
/>
</div>
)
}

Try it for yourself

I’ve prepared this sandbox for you to try it yourself.

Notice that the controlled component causes re-render each time the rating changes, while the uncontrolled version doesn’t cause re-renders at all.

This is why I prefer to use uncontrolled components by default.