Understanding Controlled and Uncontrolled Components in React
This article explains the difference between controlled and uncontrolled components in React, illustrating when to use each approach, how defaultValue and value work, and provides practical code examples and a custom hook to support both modes in component development.
Front‑end developers often need to handle forms or input components such as calendars. In React, the key distinction is between controlled and uncontrolled modes.
Uncontrolled mode means the value is changed only by the user; the component may receive an initial defaultValue but the code does not directly modify the value. The component can read the current value via onChange or a ref .
Controlled mode means the value is driven by the component’s state. The code supplies a value prop and updates it (usually with setState ) after handling onChange , causing a re‑render each time the user types.
Most simple cases work fine with uncontrolled components because you only need the user’s input. Controlled components are useful when you must transform the input before storing it, or when you need to synchronize the value with a parent component or a form store.
Below are minimal examples.
Uncontrolled example (using defaultValue and onChange ):
import { ChangeEvent } from "react";
function App() {
function onChange(event: ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
}
return
;
}
export default App;Controlled example (using value and setState ):
import { ChangeEvent, useState } from "react";
function App() {
const [value, setValue] = useState('guang');
function onChange(event: ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
setValue(event.target.value);
}
return
;
}
export default App;To support both modes in a reusable component, the common pattern is to accept both value and defaultValue props, determine the mode by checking whether value is undefined , and manage an internal state only when uncontrolled.
import { useEffect, useRef, useState } from "react";
interface CalendarProps {
value?: Date;
defaultValue?: Date;
onChange?: (date: Date) => void;
}
function Calendar(props: CalendarProps) {
const { value: propsValue, defaultValue, onChange } = props;
const [value, setValue] = useState(() => propsValue !== undefined ? propsValue : defaultValue);
const isFirstRender = useRef(true);
useEffect(() => {
if (propsValue === undefined && !isFirstRender.current) {
setValue(propsValue);
}
isFirstRender.current = false;
}, [propsValue]);
const mergedValue = propsValue === undefined ? value : propsValue;
function changeValue(date: Date) {
if (propsValue === undefined) setValue(date);
onChange?.(date);
}
return (
{mergedValue?.toLocaleDateString()}
changeValue(new Date('2024-5-1'))}>2023-5-1
changeValue(new Date('2024-5-2'))}>2023-5-2
changeValue(new Date('2024-5-3'))}>2023-5-3
);
}
export default Calendar;Many UI libraries (Ant Design, Arco Design, etc.) implement this pattern internally, often exposing a helper hook such as useMergedValue or useControllableValue . A custom hook can be written to encapsulate the logic:
function useMergeState
(defaultStateValue: T, props?: { defaultValue?: T; value?: T; onChange?: (value: T) => void; }) {
const { defaultValue, value: propsValue, onChange } = props || {};
const isFirstRender = useRef(true);
const [stateValue, setStateValue] = useState
(() => {
if (propsValue !== undefined) return propsValue;
if (defaultValue !== undefined) return defaultValue;
return defaultStateValue;
});
useEffect(() => {
if (propsValue === undefined && !isFirstRender.current) {
setStateValue(propsValue!);
}
isFirstRender.current = false;
}, [propsValue]);
const mergedValue = propsValue === undefined ? stateValue : propsValue;
const setState = useCallback((value: React.SetStateAction
) => {
const res = typeof value === 'function' ? (value as any)(stateValue) : value;
if (propsValue === undefined) setStateValue(res);
onChange?.(res);
}, [stateValue, propsValue, onChange]);
return [mergedValue, setState] as const;
}Using this hook, a component automatically works in both controlled and uncontrolled modes without the consumer needing to differentiate:
function Calendar(props: CalendarProps) {
const { value: propsValue, defaultValue, onChange } = props;
const [mergedValue, setValue] = useMergeState(new Date(), { value: propsValue, defaultValue, onChange });
const changeValue = (date: Date) => {
if (propsValue === undefined) setValue(date);
onChange?.(date);
};
return (
{mergedValue?.toLocaleDateString()}
changeValue(new Date('2024-5-1'))}>2023-5-1
changeValue(new Date('2024-5-2'))}>2023-5-2
changeValue(new Date('2024-5-3'))}>2023-5-3
);
}
export default Calendar;In summary, choose uncontrolled mode for simple input capture, and controlled mode when you need to process or synchronize the value. Implementing both in reusable components improves flexibility and aligns with the patterns used by major React UI libraries.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.