-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Framework: persist redux state #2754
Conversation
2388d6e
to
b523d2c
Compare
ack! functions in the Redux tree! |
Yeah, these should probably be refactored to not contain functions. |
} ); | ||
|
||
reduxStoreReady( reduxStore ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering how central this loader will be I think it would be really helpful to declare the handler function outside of the call to getItem()
. We should definitely have tests to handle bizarre cases here.
On that point, I think we could also benefit from using the Promise interface to localforage
because it would allow us to build up more robust error-handling without mixing the logic with our successful data handling.
local forage.getItem()
.then( loadInitialState )
.catch( handleStateLoadError );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. I was also thinking that each module should also verify the initial state that gets passed to it, and add related tests in each module.
A couple things that spring to mind here too:
|
let state = Object.assign( {}, reduxStore.getState() ); | ||
//remove dirty parts of the redux tree | ||
delete state.sites; | ||
delete state.themes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part doesn't look too scalable here. We could mimic the whole reducer tree here with an initialization/pruning tree, which could bring about the same scalable unity of the state itself.
const pruneSites = () => undefined; // could be more intelligent here
const pruneThemes = () => undefined;
const pruneState = state => Object.assign( {}, state, {
sites: pruneSites( state.sites ),
themes: pruneThemes( state.themes )
} );
or we could make a second iteration here to abstract it further
import { fromPairs } from 'lodash';
const pruners = {
sites: pruneSites,
themes: pruneThemes
};
const pruneState = staleState => {
const prunedState = formPairs( Object
.keys( pruners )
.map( key => [ key, pruners[ key ]( state[ key ] ) ] ) );
return Object.assign( {}, staleState, prunedState );
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we would remove the deletes when the redux tree is tidied up, but we can certainly employ something smarter if we only wish to persist portions of the tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my point here was mainly to suggest separating out the delete-logic from the loading logic because it seems like we could end up with a mess here if more parts of the tree need to be deleted. in my recommendations above, we would simply replace those delete
lines by this call:
let state = Object.assign( {}, reduxStore.getState() );
state = pruneState( state );
Now we can go on and do whatever we want to prune, but it won't interfere what's happening here. Also, as we get the ability to remove those delete
s, this code won't be affected
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrote up some fiddles to test: Indexed DB seems to actually support cycles and custom JS objects, which is interesting. |
One other thing to think about: How do we deal with old state trees? If I use the app today and spin out a state tree to storage, then spin the app up again in a couple months, how do we migrate the state? Or do we just throw it out? |
@blowery Good point. We most likely need to version our tree, and bump it each time a breaking data change occurs (not just an additive property). It may be easier to maintain if we do this per module. If the version doesn't match, we build with no initial state. |
mockery.registerMock( 'lib/themes/middlewares.js', { | ||
// stub redux middleware | ||
// see: http://rackt.org/redux/docs/advanced/Middleware.html | ||
analyticsMiddleware: store => next => action => { return next( action ); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the return
kind-of breaks the beautiful chain!
analyticsMiddleware: store => next => action => next( action );
of course, isn't the last part of the chain here unnecessary anyway? I think that the call after next =>
just returns next
analyticsMiddleware: store => next => next;
Here, next
gets returned and if you call the returned result, as it is written, it returns a function that accepts a single argument, which it calls action
, then calls next( action )
. In my shortened version, it actually returns the next
function itself and if you call it again and happen to pass action
, it will return the result of passing action
into next
, which is equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 updated in 81999f8
b523d2c
to
81999f8
Compare
thanks @gwwar for your work on this. The introduction of |
This scares me a bit. I left a comment in the code about these kinds of breaks and testing for them. I think we can do better, detecting invalid constructions and handling them appropriately (which could simply be flushing the tree and starting over). If we start versioning, it could get messy real quickly considering how many changes we make to Calypso in a given week. |
Conversely, if we throw out the state tree on version mismatches, we lose a good hunk of the benefit of having it stored in the first place, for the same reason, our overall version numbers change a ton. |
If we choose not to version I would suggest holding redux state for a short amount of time. In my experience dealing with persisted unversioned documents can get messy very quickly. |
it seems like we could build on the Additionally, that loader could validate the stored copy and determine whether we need to wipe it clean or if we can pick and choose what is already stored. In fact, couldn't we define a global action type, such as |
I think you want that to happen as part of the load, not after the load, otherwise you'll have a transient bit of bad state in the tree. Or maybe I'm misunderstanding what you mean? Maybe VALIDATE_AND_PRUNE is what actually does the work of getting the loaded data through the reducer and into the global state? That would work for me... |
All the plusses I have to give to that. 💯 Unversioned docs over time are a nightmare. |
@blowery, this might be moot, since as I understand it, redux dispatching is synchronous: loadCachedState() {
let state = loadFromLocal()
store = createStore( state )
store.dispatch( VERIFY_AND_PRUNE )
} so I was still suggesting that the sequence stay within the load function, but merely that the implementation of the pruning be distributed among the separate reducers |
9d19bf1
to
81d7189
Compare
The change to postpone schema validation to a later PR is a sensible one. I've tested in latest chrome, safari, and firefox and it's looking and working well for me. I've also tested with |
81d7189
to
1d493e6
Compare
Persisting Redux State
As part of the offline investigation made in our Miami Calypso Core Meetup, this PR persists redux state using localforage, and loads it as the initial state on boot. This may provide us with some minor performance improvements, and compliments our offline use case. We currently clear this state when users log off.
Blockers( #2757, #2759 ) the redux tree is currently not "pure". Some sections of the tree contain functions, which means that errors are thrown during serialization. Unless we wish to write custom serializers/deserializers, the redux tree should be cleaned up to ensure that it only holds simple objects that do not contain blobs or functions.Update: I'm currently working around this by adding in
SERIALIZE
andDESERIALIZE
actions that are triggered during loading/before writing to localforage. These actions are not dispatched normally and will not modify the existing store. It instead modifies the copy of current state before we write to localforage, and the initialState object we use when creating a store respectively.In each reducer,
SERIALIZE
takes what's in memory and returns a simple JS object. Similarly inDESERIALIZE
the reducer is given state that is a simple JS object. It should validate if the data is valid using a schema. If it is valid, further manipulate the JS object to match what your subtree expects in memory (this might be a no-op, or say instantiation of Immutable.js objects). If the data is invalid, return the default initial state.Open questions
Action Items
SERIALIZE
andDESERIALIZE
for many sub-trees. I will create an issue for each piece of the subtree to implement persistence.Subtree Status
Testing instructions
Persisted sub-trees to test, with pings for folks who have the most changes in the reducers:
cc @rralian @mtias @retrofox @artpi @aduth @dmsnell