Redux state hydration and schema mismatch during preloadedState injection
0 reputation · 15 Jun 2022, 13:07 UTC
0 reputation · 15 Jun 2022, 13:07 UTC
Redux allows for the recovery of application state via the preloadedState option in configureStore or createStore. This mechanism is commonly used to hydrate the store from serialized JSON stored in localStorage or IndexedDB.
A challenge arises when the structure of the persisted state tree no longer aligns with the current application's initial state definition due to version updates. Because Redux core does not provide a native versioning or migration schema, injecting an outdated state object can lead to undefined properties or runtime errors in reducers expecting a newer state shape.
What is the recommended pattern for validating the integrity of recovered data before it is passed to preloadedState? How should state reconciliation be handled to ensure backward compatibility between different serialized state versions?
35275 reputation · 15 Jun 2022, 18:58 UTC
Do not pass raw parsed JSON directly to preloadedState. Treat persisted storage as untrusted input: parse it defensively, check a reserved schema version, run ordered migrations, validate the result, and only then pass the normalized object to configureStore or createStore.
Redux core has no native versioning or migration API. createStore and configureStore accept preloadedState, and reducers receive undefined for missing slices. A reducer that defaults its parameter to an initial state object survives missing keys safely. The common failure is an old persisted tree missing a newly added slice, so code like state.newFeature.field throws because state.newFeature is undefined. Extra top-level keys are usually ignored by combineReducers, but a custom root reducer may preserve them.
__schemaVersion.JSON.parse inside try/catch. On malformed JSON or unexpected types, fall back to the initial state instead of creating the store with bad data.0.preloadedState.function loadPersistedState() {
try {
const raw = localStorage.getItem("reduxState");
if (!raw) return undefined;
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null) return undefined;
return migrate(parsed);
} catch {
return undefined;
}
}
function migrate(state) {
const version = state.__schemaVersion ?? 0;
let next = { ...state };
if (version < 1) next = migrateV0ToV1(next);
if (version < 2) next = migrateV1ToV2(next);
return { ...next, __schemaVersion: CURRENT_VERSION };
}
const store = configureStore({
reducer: rootReducer,
preloadedState: loadPersistedState(),
});
This assumes every slice reducer uses default parameters so undefined preloaded state resolves to the initial state. Exact warnings for unknown preloadedState keys differ between Redux versions, Redux Toolkit, and custom root reducers; verify against your installed version. Do not store functions, secrets, or derived server state in client storage.
Verification: create a store with an old persisted object missing a new slice and assert no error is thrown and the new slice equals its reducer initial state. Then persist v0 JSON, upgrade the schema, run the migration, and assert the hydrated state matches the expected migrated shape. Confirm invalid JSON falls back to initial state.
One missing detail that could change the recommendation: are you using vanilla Redux createStore, Redux Toolkit configureStore, or a custom root reducer? Unknown-key handling differs across those configurations.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 15 Jun 2022, 23:34 UTC
To prevent runtime errors during hydration, a robust pattern is to wrap the persisted state in a versioned envelope rather than storing the state tree directly. This allows the application to detect schema drift before the data ever reaches configureStore.
{ version: number, state: object }.migrate_2_to_3 and migrate_3_to_4.initialState to avoid partial hydration crashes.It is important to note that combineReducers does not automatically merge missing nested properties from the initial state into a provided preloadedState slice; if a slice is present but outdated, the reducer receives that stale object exactly as is.