NgRx state persistence de-serialization failure under production minification
0 reputation · 02 Sept 2021, 21:30 UTC
0 reputation · 02 Sept 2021, 21:30 UTC
When implementing a custom meta-reducer for state persistence, functional discrepancies often emerge between local development and production environments. While the local environment successfully hydrates the state from local storage, the production build frequently encounters runtime errors or inconsistent data during the initialization phase.
The issue appears linked to the aggressive minification and property mangling performed during production-grade builds. If the de-serialization logic relies on specific object keys that are altered during the build process, the meta-reducer fails to map the stored string back to the current state model. Furthermore, there is a lack of a native, built-in versioning schema for persisted objects, making it difficult to handle schema mismatches when a user returns to the app with stale data in their browser.
Angular production builds do not rename object property names used for NgRx state, so disabling minification is not required to protect serialized keys. The hydration failures you see in production are almost always prototype loss from JSON serialization and missing schema versioning, not property mangling.
Two practical fixes for this case:
Verify the build first, then enforce plain state.
grep -o "userProfile" dist/*.js
__type instead of relying on class names.Store a versioned envelope and apply ordered, slice-scoped migrations.
// persisted shape: { version: number, state: RootState }
Meta-reducer pattern:
const CURRENT_VERSION = 2;
function migrate(stateEnvelope) {
let version = stateEnvelope?.version ?? 0;
let state = stateEnvelope?.state ?? {};
if (version < 1) state = migrateV0toV1(state);
if (version < 2) state = migrateV1toV2(state);
return { version: CURRENT_VERSION, state };
}
One diagnostic that changes the recommendation: does your persisted payload contain class instances such as Date or custom models? If yes, the fix is serialization normalization, not key protection.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 03 Sept 2021, 01:19 UTC
Store a small wrapper instead of raw state slices:
interface PersistedEnvelope {
version: number;
payload: T;
migratedAt?: number; // epoch ms of last successful migration
}
The meta-reducer reads the envelope, compares version to CURRENT_VERSION, and pipes the payload through an ordered array of pure migration functions migrations[v] : (payload) => payload. Each migration returns a new object; the original localStorage entry is only overwritten after the full chain succeeds.
Feed every historic JSON fixture (including malformed/partial data) into the reducer and assert the output matches the current State interface. A single parametrized test catches missing migration steps before they reach production.
String literal keys in the envelope ("userProfile") survive minification. The risk appears when code uses shorthand property names ({ userProfile }) that Terser may rename; always reference persisted keys via a constant or explicit string literal in the hydration logic.