Repository navigation
Sync storage engines, nested rehydrate after purge, frozen state, key-change warning - #1522
Merged
Merged
Conversation
…1281) persistStore called .then on whatever getItem returned, and the write path called .catch on setItem's result, so a synchronous storage engine, or a test mock like jest.fn() that returns undefined, crashed with "Cannot read properties of undefined (reading 'then')". Wrap each storage call in a promise: a returned value, a promise and a thrown error all work, and a throw is handled like a failed read or write. setItem is still called synchronously, so flush() in beforeunload keeps writing right away.
After persistor.purge(), persistReducer ignores REHYDRATE so purged data doesn't come back, but it ignored REHYDRATE for every key, not just its own, and didn't pass it on. A nested persistReducer whose read finished after the purge (logging out right at startup), or one added later with code splitting, never saw its REHYDRATE: it never rehydrated, so it never saved again. Ignore only this key's REHYDRATE and pass the rest through.
persistReducer adds _persist by returning a new top-level object, so with Redux Toolkit (immer), which freezes the state it produces, every level was frozen except the top one, and assigning to a top-level key silently worked. When the reducer's state is frozen, freeze the object that adds _persist too. Unfrozen state is left as is.
Swapping in a persistReducer with another key or keyPrefix through replaceReducer (a common attempt at per-user storage) keeps the current state and starts writing it under the new key, replacing what's stored there; nothing is loaded from it. Log a development error when that happens, pointing to a new store and persistor instead. Hot reloading with the same key is unaffected.
rt2zz
force-pushed
the
fix-storage-robustness
branch
from
October 7, 2026 19:03
3f72e27 to
7241d3c
Compare
From review of the previous commit: once a purged parent passes a nested persistReducer's REHYDRATE through, a read that started before the purge could restore the purged data. The nested REHYDRATE gave the parent a new child _persist, so the parent saved its whole pre-purge state (when it also stores the child); and plain reducers reading that REHYDRATE got the purged payload. Logging out with purge() and reloading right away left the old data in storage. A read that finishes after its store was purged now delivers no payload, and a purged store passing a nested REHYDRATE through doesn't save; the next real change saves as usual.
A storage read rejected with no reason (an engine calling reject(), or, since the previous commits, throwing null) reached the read-failure path with err undefined, so it was taken for empty storage and initial state was saved over the stored data. Substitute an error, for reads and for migrations. Also, a getItem(key, callback) that returns nothing is a callback-style engine, which isn't supported; returning nothing used to crash on .then, and since storage values are accepted it would have read as empty storage. It's now a failed read with a message saying so.
From review: the previous commit froze the top level only once the reducer produced frozen state, i.e. after the first action a slice handled. persistReducer passes its reducer a copy without _persist, which Redux Toolkit returns as is when no case reducer matches, and the state reconcilers return copies too. Also check whether the state persistReducer was given is frozen, and use the same helper for the purged REHYDRATE branch.
This was referenced Oct 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1397, closes #1281, closes #1298.
Stacked on #1521 (it touches the same
REHYDRATEbranch); merge that first. One change per commit:persistStorecalled.thenon whatevergetItemreturned, so a synchronous engine or a Jest mock (jest.fn()returnsundefined) crashed with "Cannot read properties of undefined (reading 'then')". Each storage call is now wrapped in a promise: a value, a promise and a throw all work, and a throw is handled like a failed read or write.setItemis still called synchronously, soflush()inbeforeunloadkeeps writing right away.REHYDRATEthrough. Afterpersistor.purge(), a persistReducer ignoredREHYDRATEfor every key, not just its own, and didn't pass it on. So a nested persistReducer whose read finished after the purge (logging out right at startup) never rehydrated and never saved again. So did one added later with code splitting. Found while checking After purge it stops persisting #883 against v7; v6 had it too.REHYDRATEpassing through could make a parent that also stores the child save its whole pre-purge state, and plain reducers reading thatREHYDRATEgot the purged payload. Such a read now delivers no payload, and a purged store passing a nestedREHYDRATEthrough doesn't save.reject()with no error was taken for empty storage, so initial state was saved over the stored data. An engine in our community list does this. Same for migrations. A callback-stylegetItem(key, cb)that returns nothing is now a failed read with a clear message too, instead of reading as empty storage._persist. It's now frozen when the reducer's state, or the state persistReducer was given, is frozen. Unfrozen state is untouched.replaceReducerchanges the storage key (Dynamic keyPrefix for Multiple Users #1112). Swapping in a persistReducer with anotherkey/keyPrefix(a common attempt at per-user storage) keeps the current state and writes it over what's stored under the new key, without loading anything. That's now a development error pointing to a new store and persistor; the docs PR has that recipe. Hot reloading with the same key doesn't warn.Tests:
storageValues.spec.ts,purgeNested.spec.ts,frozenState.spec.ts,replaceKey.spec.ts. Each fails without its commit (except the "doesn't freeze" and "doesn't warn" guards). 157 tests pass across repeated runs.An adversarial review ran scenario scripts covering:
The purge, rejection and freeze commits address what it found. Known limits, not fixed here:
keywhile Redux DevTools replays stops saving silently, though stored data stays intactNo dependency changes.