Max Schmitt

March 23 2023

useControlledProps: Make any React Component Controlled/Uncontrolled

A while ago I wrote about creating React components that can be both controlled and uncontrolled.

In the meantime, I've created a reusable React Hook for this pattern: useControlledProps().

Example

Here is how you would use it to implement a reusable text input with character count.

TSX

function CharacterCountInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
const { value, ...rest } = useControlledProps(props)
return (
<>
<input value={value} {...rest} />
{value.length}
</>
)
}

Now you can use this <CharacterCountInput> component in both a controlled or uncontrolled way:

TSX

function App() {
const [value, setValue] = useState('')
const onChange: React.ChangeEventHandler = (e) => {
setValue(value)
}
return (
<>
{/* Controlled: */}
<CharacterCountInput value={value} onChange={onChange} />
{/* Uncontrolled: */}
<CharacterCountInput />
</>
)
}

Code

Here is the code for the hook, feel free to copy-paste it into your project!

useControlledProps.ts

type ControlledElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
interface ControlledProps<El extends ControlledElement> {
value?: string | ReadonlyArray<string> | number | undefined
onChange?: React.ChangeEventHandler<El> | undefined
defaultValue?: string | number | ReadonlyArray<string> | undefined
}
function useControlledProps<El extends ControlledElement, T extends ControlledProps<El>>({
value: valueFromProps,
onChange: onChangeFromProps,
defaultValue,
...rest
}: T) {
const isControlled = typeof valueFromProps !== 'undefined'
const hasDefaultValue = typeof defaultValue !== 'undefined'
const [internalValue, setInternalValue] = useState<T['value']>(hasDefaultValue ? defaultValue : '')
const value = isControlled ? valueFromProps : internalValue
const onChange = useCallback<React.ChangeEventHandler<El>>(
(e) => {
// When the user types, we will call props.onChange if it exists.
// We do this even if there is no props.value (and the component
// is uncontrolled.)
if (onChangeFromProps) {
onChangeFromProps(e)
}
// If the component is uncontrolled, we need to update our
// internal value here.
if (!isControlled) {
setInternalValue(e.target.value)
}
},
[isControlled, onChangeFromProps]
)
return { value: value as string, onChange, ...rest }
}

Check out the CodeSandbox for a complete example.