usePersistedState
A mutable value backed by asynchronous storage. It reads the persisted value once on mount, exposes it as a writable .value, and persists every write in the background. It returns a PersistedState<T> that reports both whether the initial read has finished (isInitialized) and whether the latest write has landed (isSynchronized).
class ThemeToggle extends HookWidget {
const ThemeToggle({super.key, required this.store});
final KeyValueStore store;
Widget build(BuildContext context) {
// get runs once on mount; set is called on every write and persists in the background.
final darkModeState = usePersistedState<bool>(
() async => (await store.read('dark_mode')) == 'true',
(value) async =>
value == null ? store.delete('dark_mode') : store.write('dark_mode', '$value'),
);
if (!darkModeState.isInitialized) return const CircularProgressIndicator(); // <- still reading
return SwitchListTile(
value: darkModeState.value ?? false, // <- value is nullable: null until first read / after a null write
onChanged: (it) => darkModeState.value = it, // <- updates immediately, persists in the background
title: Text(darkModeState.isSynchronized ? 'Saved' : 'Saving...'), // <- false while a write is in flight
);
}
}
Signature
PersistedState<T> usePersistedState<T extends Object>(
Future<T?> Function() get,
Future<void> Function(T? value) set, {
bool canGet = true,
HookKeys getKeys = hookKeysEmpty,
});
abstract interface class PersistedState<T extends Object> implements MutableValue<T?>, HasInitialized {
abstract final bool isSynchronized;
}
get- reads the stored value. Run on mount and whenevergetKeyschange. May returnnull(nothing stored yet).set- persists a value. Called on every write to.value; receivesnullwhen the value is cleared.canGet(trueby default) - gates the read. Whilefalse, the read is skipped (and the state cleared); when it turnstrue,getruns.getKeys- changing them re-runsget, the same wayuseAutoComputedState'skeysdo.value- read the current value (nullable:nulluntil the first read completes, and after anullwrite). Writing it updates the value immediately and kicks offsetin the background.isInitialized-trueonce the initial read has completed.isSynchronized-truewhen the read has completed and no write is in flight - i.e. the in-memory value matches storage.
Internally this hook composes a useAutoComputedState (the read) with a useSubmitState (the write), which is why canGet / getKeys mirror the computed-state gate and isSynchronized tracks the submit's inProgress.
Use cases
-
Local preferences and small bits of device-local state - theme, onboarding-seen flags, a last-selected tab - stored in
SharedPreferences, Hive, secure storage, or any async key-value store. -
A cache-then-network read: serve the persisted value instantly, fetch fresh data, and write what comes back. Pair
usePersistedState(the cache) with auseAutoComputedState(the network); the fresh value wins, the cache fills the gap, and a failed fetch leaves the cache untouched:// Show the cached value immediately, refresh from the network, persist what came back.Profile? useCachedProfile({required KeyValueStore store, required ProfileApi api}) {final cachedState = usePersistedState<Profile>(() async => (await store.read('profile'))?.let(Profile.fromJson),(value) async =>value == null ? store.delete('profile') : store.write('profile', value.toJson()),);// A failed fetch leaves the cache untouched.final freshState = useAutoComputedState(api.fetchProfile, isRetryable: true);// Writing cachedState.value also persists it.useEffect(() {final value = freshState.valueOrNull;if (value != null) cachedState.value = value;return null;}, [freshState.valueOrNull]);return freshState.valueOrNull ?? cachedState.value; // <- network wins; cache fills the gap}
Caveats
-
valueis nullable andTis non-null. The stored type isT extends Object, butvalueisT?because nothing may be stored yet. Handle thenull(during the initial read and after a clear) rather than forcing it. -
Reads run on mount, writes run on assignment. Setting
.valuedoes not wait forsetto finish - it updates in memory immediately and persists in the background. WatchisSynchronizedif you need to show a "saving..." state or block navigation until the write lands. -
canGetis for logical prerequisites, likeuseAutoComputedState'sshouldCompute- flipping it tofalseclears the state. Gate on facts ("a user id exists"), not transient conditions like connectivity. -
A failed write is wrapped as
Retryable(the submit state's default) and rethrows, so it surfaces in the error pipeline rather than being silently lost. The in-memory.valuestill reflects your write; reconcile if the persistence layer can reject it.
See also
- useAutoComputedState - the read half, and the network side of cache-then-network
- useSubmitState - the write half behind every
.valueassignment - useState - in-memory state with no persistence