Skip to main content
Version: Next

Persisted vs Ephemeral State

useMnemonicKey will persist whatever you pass to set. That is powerful, but it also means complex UI modules can accidentally store runtime-only state that should have disappeared after a reload. Persist only the values you want to survive reload and restore intentionally. If rehydrating a field would feel surprising, it should probably stay ephemeral.

The anti-pattern

The most common mistake is storing one big UI object that mixes durable preferences with transient runtime details:

const { value, set } = useMnemonicKey("inbox-ui", {
defaultValue: {
theme: "dark",
density: "comfortable",
sidebarOpen: false,
searchDraft: "",
hoveredMessageId: null,
},
});

That looks convenient, but now sidebarOpen, searchDraft, and hoveredMessageId can come back after a full reload.

Persist the durable slice, and keep transient UI state in plain React state:

type PersistedInboxPrefs = {
theme: "light" | "dark";
density: "comfortable" | "compact";
};

type EphemeralInboxUi = {
sidebarOpen: boolean;
searchDraft: string;
hoveredMessageId: string | null;
};

const { value: prefs, set: setPrefs } = useMnemonicKey<PersistedInboxPrefs>("inbox-prefs", {
defaultValue: {
theme: "dark",
density: "comfortable",
},
});

const [ui, setUi] = useState<EphemeralInboxUi>({
sidebarOpen: false,
searchDraft: "",
hoveredMessageId: null,
});

This keeps reload behavior intentional:

  • prefs.theme rehydrates
  • prefs.density rehydrates
  • ui.sidebarOpen resets
  • ui.searchDraft resets
  • ui.hoveredMessageId resets

Helper pattern: usePersistentSlice

This is an application-level helper built on top of useMnemonicKey. It is not exported by react-mnemonic, but it is a useful pattern when many screens need the same durable-vs-ephemeral split.

function usePersistentSlice<Persisted extends object, Ephemeral extends object>(
key: string,
options: {
defaultPersistent: Persisted;
defaultEphemeral: Ephemeral;
},
) {
const {
value: persisted,
set: setPersisted,
reset: resetPersisted,
} = useMnemonicKey<Persisted>(key, {
defaultValue: options.defaultPersistent,
});

const [ephemeral, setEphemeral] = useState<Ephemeral>(options.defaultEphemeral);

const updatePersisted = <K extends keyof Persisted>(field: K, value: Persisted[K]) => {
setPersisted((prev) => ({ ...prev, [field]: value }));
};

const updateEphemeral = <K extends keyof Ephemeral>(field: K, value: Ephemeral[K]) => {
setEphemeral((prev) => ({ ...prev, [field]: value }));
};

return {
persisted,
ephemeral,
updatePersisted,
updateEphemeral,
resetPersisted,
};
}

Use this when you want a small reusable convention in app code without turning every runtime detail into persisted state.

Fields that should usually stay ephemeral

  • Loading flags and optimistic mutation status
  • Validation errors and “is dirty” form metadata
  • Hovered, focused, expanded, dragged, or selected UI state
  • Sort previews, drag position, and in-progress gestures
  • Temporary search drafts and filters unless they are explicitly user preferences
  • Server response caches or “last fetched at” timestamps unless you intentionally want them restored

Interactive example

Type into both panels below, toggle the sidebar, then reload the page.

  • In the anti-pattern panel, the transient fields come back because they were stored with the durable fields.
  • In the recommended panel, only the persisted preferences come back.
Durable values should come back after reload. Transient UI state should only survive when you intentionally keep it in memory for the current session.

Anti-pattern: persist the whole UI object

Surprising rehydration

Theme and density belong in storage, but this shape also persists transient UI fields like search text and whether the sidebar is open.

Reload behavior: everything below comes back, including the open sidebar and search draft.
{
  "theme": "light",
  "density": "comfortable",
  "sidebarOpen": false,
  "searchDraft": ""
}

Recommended: persist only the durable slice

Intentional reloads

Durable preferences stay in useMnemonicKey, while runtime-only UI fields stay in plain React state.

Reload behavior: theme and density rehydrate, but the sidebar and search draft reset.
Persisted slice
{
  "theme": "light",
  "density": "comfortable"
}
Ephemeral slice
{
  "sidebarOpen": false,
  "searchDraft": ""
}

What to persist

Good candidates for persistence:

  • Theme, density, and layout preferences
  • Saved filters that users expect to restore intentionally
  • User-authored drafts that should survive reload
  • Last selected account, workspace, or durable navigation context

When in doubt, start narrow. Persist the smallest durable slice first, then add more only when rehydration is clearly desirable.