Redux Toolkit preloadedState limits during state shape evolution
0 reputation · 24 Oct 2022, 18:33 UTC
0 reputation · 24 Oct 2022, 18:33 UTC
When using configureStore in Redux Toolkit, the preloadedState property allows for the injection of recovered data from a serialized snapshot during store initialization. This mechanism is essential for restoring application state from persistent storage.
A challenge arises when the application's reducer logic or state shape evolves between the time the state was persisted and the time it is restored. Because preloadedState is merged into the store, there is a risk of hydrating the store with stale data structures that no longer align with the current reducer definitions.
Given that Redux does not natively provide a versioning or migration layer for preloaded state, the responsibility for data verification falls on the implementation logic.
preloadedState before it is passed to the store?33275 reputation · 25 Oct 2022, 03:47 UTC
Redux Toolkit does not version, migrate, or validate preloadedState. It passes the object straight to your root reducer. To prevent incompatible snapshots from breaking your store, wrap persisted data in a versioned envelope, run migrations before store creation, and validate the result with a schema check. If either step fails, discard the snapshot and fall back to undefined so reducers initialize with their defaults.
Confirmed: configureStore({ preloadedState }) forwards that value to the root reducer. If your reducer expects user.profile.name but the persisted shape has user.name, the reducer receives undefined and may throw or silently store wrong data.
Likely explanation for hard-to-debug crashes: old snapshots contain obsolete keys or changed types (for example, a string where an array is now expected). Because Redux does not automatically merge or prune the incoming state, stale fields persist in the tree and can cause runtime errors when components assume the new shape.
{ version: 3, state: serializedRootState } instead of the bare state. Increment version whenever a reducer's shape changes.undefined rather than passing bad data to configureStore.configureStore({ reducer, preloadedState: validated }).Example skeleton:
const CURRENT_VERSION = 3;
const migrations = {
1: (s) => ({ ...s, user: { profile: { name: s.user.name } } }),
2: (s) => ({ ...s, settings: { theme: s.settings.theme ?? 'light' } }),
};
function loadState() {
const raw = localStorage.getItem('appState');
if (!raw) return undefined;
try {
const parsed = JSON.parse(raw);
let state = parsed.state;
for (let v = parsed.version; v < CURRENT_VERSION; v++) {
const migrate = migrations[v];
if (!migrate) return undefined;
state = migrate(state);
}
return RootStateSchema.parse(state);
} catch {
return undefined;
}
}
const store = configureStore({
reducer,
preloadedState: loadState(),
});
To confirm this works in your app: persist a v1 snapshot, bump CURRENT_VERSION, reload, and inspect store.getState(). The migrated keys should match the current reducer's initial shape, and no obsolete keys should remain.
Is your hydration synchronous (e.g., localStorage) or asynchronous (e.g., IndexedDB, network fetch)? If asynchronous, preloadedState cannot be used directly; you would initialize the store with defaults and dispatch a rehydration action after the promise resolves.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.