Skip to main content

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 whenever getKeys change. May return null (nothing stored yet).
  • set - persists a value. Called on every write to .value; receives null when the value is cleared.
  • canGet (true by default) - gates the read. While false, the read is skipped (and the state cleared); when it turns true, get runs.
  • getKeys - changing them re-runs get, the same way useAutoComputedState's keys do.
  • value - read the current value (nullable: null until the first read completes, and after a null write). Writing it updates the value immediately and kicks off set in the background.
  • isInitialized - true once the initial read has completed.
  • isSynchronized - true when 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 a useAutoComputedState (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

  • value is nullable and T is non-null. The stored type is T extends Object, but value is T? because nothing may be stored yet. Handle the null (during the initial read and after a clear) rather than forcing it.

  • Reads run on mount, writes run on assignment. Setting .value does not wait for set to finish - it updates in memory immediately and persists in the background. Watch isSynchronized if you need to show a "saving..." state or block navigation until the write lands.

  • canGet is for logical prerequisites, like useAutoComputedState's shouldCompute - flipping it to false clears 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 .value still reflects your write; reconcile if the persistence layer can reject it.

See also