useSyncedState
useState that updates to the new value when the initial state changes.
This useSyncedState
hook provides a convenient way to create a piece of state that stays synchronized with an external value (initialState
) even if that value changes over time. Unlike useState
, which only uses the initial value on the first render, useSyncedState
watches for changes to initialState
and updates the internal state accordingly.
This is especially useful when you need both of the following:
- A controlled initial value that can be updated externally (e.g., from props or context), and
- The ability to call
setState
locally to override the value temporarily in response to user actions.
By calling setState(initialState)
inside a useEffect
that depends on initialState
, the hook ensures that whenever the input changes, the internal state resets to reflect the new source of truth. This pattern is important in scenarios like forms or editable components that need to reset when upstream data updates.
The hook returns a [state, setState]
tuple identical to useState
, so you can continue to treat it as regular state in your component logic.
Important:
You should only use this hook if you need the ability to call the setter. If you just want to derive a value that updates when dependencies change but don’t need to set it yourself, prefer useMemo
for simpler and more predictable behavior.
Code
import { useEffect, useState } from 'react';
/**
* Just like a useState, but if the initial state is changed, the state will be updated to the new value.
* @important — You should only ever use this hook if you need to use the setter, otherwise you should just useMemo the value you want to sync.
*/
const useSyncedState = <T>(initialState: T) => {
const [state, setState] = useState<T>(initialState);
useEffect(() => {
setState(initialState);
}, [initialState]);
return [state, setState] as const;
};
export default useSyncedState;
Example & Usage
Parent container just uses a useState
and passes it through to the inner container:
const BasicExample = () => {
const [count, setCount] = useState(0);
return (
<Container>
<div className="flex shrink-0 flex-grow flex-col">
<p className="text-muted-foreground mb-2 text-sm">
Start counting from...
</p>
<RadioGroup
onValueChange={(value) => setCount(Number(value))}
className="grid grid-cols-3 gap-2"
>
<div className="flex flex-row items-center gap-2">
<RadioGroupItem value="0" id="start-from-0" />
<Label htmlFor="start-from-0">0</Label>
</div>
<div className="flex flex-row items-center gap-2">
<RadioGroupItem value="10" id="start-from-10" />
<Label htmlFor="start-from-10">10</Label>
</div>
<div className="flex flex-row items-center gap-2">
<RadioGroupItem value="100" id="start-from-100" />
<Label htmlFor="start-from-100">100</Label>
</div>
</RadioGroup>
</div>
<ExampleButton initialCount={count} />
</Container>
);
};
The inner container uses useSyncState
as described above, so when we choose a different starting point it'll update the count for the bottom, but the changes in the button do not impact the parent state.
const ExampleButton = ({ initialCount }: ExampleButtonProps) => {
const [count, setCount] = useSyncedState(initialCount);
return (
<Container className="bg-background w-full">
<Button onClick={() => setCount(count + 1)}>{count}</Button>
</Container>
);
};
Start counting from...