NgRx state schema compatibility during re-hydration rollbacks
0 reputation · 14 Sept 2023, 08:39 UTC
0 reputation · 14 Sept 2023, 08:39 UTC
NgRx currently lacks built-in schema versioning or automated migration paths for persisted state. When a state shape evolves between application versions, rehydrating legacy data from localStorage often leads to runtime errors or inconsistent UI behavior if selectors expect properties not present in the older schema.
The EntityEntity adapter is particularly vulnerable to these changes; modifying the idField or restructuring the entity dictionary requires manual re-mapping to prevent data loss. While custom meta-reducers can be used to intercept and transform the state tree during initialization, there is no standardized pattern for handling rollbacks where the application version reverts below the persisted state version.
What is the most effective strategy for implementing version-checking within a meta-reducer to ensure backward compatibility without degrading startup performance? How should selectors be designed to gracefully handle missing fields when the persisted state does not match the current TypeScript interfaces?
31775 reputation · 14 Sept 2023, 12:20 UTC
The most effective strategy for handling schema evolution in NgRx is a version-aware meta-reducer that intercepts the re-hydrated state before it reaches the store. To prevent runtime errors during version rollbacks (where the persisted state is newer than the application code), the meta-reducer must implement a strict version check and a purge mechanism.
The meta-reducer should act as a gatekeeper during the INIT action or the re-hydration phase. The logic follows these priorities:
version key in the persisted state against the current application's SCHEMA_VERSION constant.persistedVersion > currentVersion, the state is incompatible. The only safe operation is to purge the persisted state and return the initial state to avoid unpredictable crashes.persistedVersion < currentVersion, pass the state through a chain of migration functions (e.g., v1ToV2(), v2ToV3()) until it matches the current schema.Since TypeScript interfaces are erased at runtime, they cannot protect against missing fields in localStorage. Selectors must be designed to handle undefined or null values gracefully to prevent UI breakage.
?.) when accessing deeply nested properties in the state.?? operator to ensure the component receives a valid type (e.g., an empty array instead of undefined).// Example of a defensive selector pattern
export const selectUserPreferences = createSelector(
selectAuthState,
(state) => ({
theme: state?.preferences?.theme ?? 'light',
notificationsEnabled: state?.preferences?.notificationsEnabled ?? true
})
);
This approach assumes that the state is persisted as a JSON object containing a top-level version property. It also assumes that a total state purge is an acceptable trade-off during a version rollback to maintain application stability. If your application uses the @ngrx/entity adapter, migrations must explicitly re-map the ids and entities collections if the idField has changed.
Diagnostic Detail Needed: Are you using a third-party persistence library (like ngrx-store-localstorage) or a custom implementation? This determines whether the version check happens inside a meta-reducer or within the persistence library's configuration.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.