Closed sunyang713 closed 8 years ago
Review your data flow and conversions between immutable and js.
Update:
I did a little research on how fromJS()
and .toJS()
work. fromJS()
will stop recursively converting as soon as it hits an Immutable-type object. This means that if you have an Immutable Map or List that contains plain javascript objects/arrays, those inner objects/arrays will not be converted. This means that my trigger-happy fromJS
usage is sorta okay in the createReducer() function.
For the server side issue of using both fromJS()
and .toJS()
in the same line, I made a little helper function (borrowing Immutable source code) that simply iterates through the elements of the object and converts Immutable objects. Here's the code:
function toJS(js) {
for (let el in js) {
if (!isPlainObj(js[el]) && !Array.isArray(js[el]))
js[el] = js[el].toJS()
}
return js
}
function isPlainObj(value) {
return value && (value.constructor === Object || value.constructor === undefined);
}
With its application:
const initialState = JSON.stringify(toJS(store.getState()))
We assume the structure of the "impure json" to be a plain object with elements that may or may not be plain objects/arrays. If element isn't a plain object/array, the function assumes it's an Immutable object.
Right now I'm still trying to figure out how to handle Immutable type objects when they're injected into a connected component. More reading shows that if you plan to inject an object or array, it should remain in its Immutable form. Still would like to hear from other people though!
Okay, so I've arrived at decently clean solutions for most of the things I mentioned above. Most notably, transform the entire redux state to an immutable-js object. Typically, combineReducers()
will generate the complete initialState
for the application. This results in the 'impure' plain object consisting of immutable-js objects. redux-immutablejs gives the best solution to this. It provides a new combineReducers()
function and in general follows FSA. It also provides a more robust createReducer()
helper function.
If using react-router-redux
, simply follow the instructions here.
Now for SSR, when you grab initialState
from store.getState()
, you will have a pure immutable-js type object. If you apply JSON.stringify(initialState)
, the Map
and List
tokens won't show up in the resulting string. No need to call 'toJS()`.
On the client-side, inside configureStore()
, simply apply fromJS()
on the initialState
parameter.
Since it seems like I'm sort of just talking to myself, I'll close this issue for now. Please re-open and comment if there's a different pattern/design!
Sorry to drop into a closed issue, but I'm not finding any answer about a thing: when exactly does using an immutable structure for your state becomes more efficient than object creation and how much is that performance gain?
@alvaromb http://facebook.github.io/immutable-js/#the-case-for-immutability
I don't have any hard benchmarks though, and I don't know about the underlying code to make any informed explanations, sorry.
So, the thing is the comparison of the state when it changes, am I right? You would initially benefit from this independently of your store size I guess. Would love to know when to switch from object creation to immutable stores becomes a noticeable performance improvement because we have a couple of apps with state change with object creation.
@alvaromb - Yes, Immutable can allow you to write simpler shouldComponentUpdate logic via Immutable.is, however I'd only recommend converting to it when performance becomes a problem or you have a lot of down time. For me, the switch entailed some hefty changes to my reducer composition (lots of updates can be done more efficiently by using a selector to walk the tree). You may bring in more complexity than necessary if your app isn't struggling.
@sunyang713 - Until recently I'd been doing some weird workarounds to hydrate my state that contains a normal top level object and random immutables as subtrees. I just wrote fire-hydrant to solve the serialization / hydration issue.
It exports serialize
and fromHydrant
functions. serialize
walks the the state tree and anywhere that it finds an immutable it replaces the node with a representation that can be passed into the fromHydrant
function to recreate the initial state with immutables intact. On the client you simply call let initialState = fromHydrant(window.__initialState__)
and it will restore the state recursively.
@cchamberlain @sunyang713 Also take a look at transit-js (https://github.com/cognitect/transit-js) and transit-immutable-js (https://github.com/glenjamin/transit-immutable-js). Except for having to do some extra escaping they've worked great for serializing/deserializing immutable.js during SSR for me.
tl;dr: how do I properly use Immutable.js with redux and incorporate SSR?
final update - click here for the pattern I settled with
Hi, this is a multi-part question about using Immutable.js with redux. It's really cool that redux doesn't care how you store your state. I really want to take advantage of the performance boost from using Immutable.js as well as the unquestionable safety side-effect. Here are couple problems I ran into while implementing them, along with my attempted solutions. I guess what I'm really looking for is someone with much more experience or with premeditated ideas of how it should be implemented (Dan the Man?) to provide some pointers/guidance.
The first question starts at the lowest level - implementing reducers. Below is the simplest example using an Immutable.js object for the state.
This is fine. But say I want to decrease boilerplate and minimize the places I need to use Map({ ... }) and List(). I follow the instructions for writing a createReducer() helper function here, taking note of the line "Maybe you want it to automatically convert plain JS objects to Immutable objects to hydrate the server state".
Here is my first attempt at such a helper function:
The tweak I made for Immutable.js is simply to apply fromJS() on the initialState. In this way, I can define the initialState in the reducer file as a plain object. Here is the resulting reducer:
Now, I'm ready to connect a react-component to the store. Two high-level questions at this point:
connect
injects into the component remain an Immutable object, or should it be converted via.toJS()
? Or maybe, should thecreateReducer()
helper function return thestate.toJS()
each time? My intuition is that it should remain an Immutable object. I don't know how expensive.toJS()
is, but I think (totally guessing here) the benefits of using Immutable come from making sure the state is stored as an Immutable object in the store so that any object comparisons are fast. But maybe, iftoJS()
is not expensive, when I write themapStateToProps()
function forconnect()
, (or use reselect to select derived parts of the state), I can use.toJS()
or the regular 'getter' functions for Immutable objects, injecting plain javascript objects/types into the connected component. Or maybe prop-change -> react re-render can also benefit from Immutable objects, in that the injected props should still be Immutable objects.fromJS()
? I suppose this isn't completely a redux related question... but this question is primarily a response to "Maybe you want it to automatically convert plain JS objects to Immutable objects to hydrate the server state".Okay, so client-side redux-flow with Immutable is fine. However, server-side rendering introduces a few new challenges.
Following the server rendering recipe, one of the cool tricks is to actually write the initial state code into the served html string like so:
The
initialState
parameter comes from creating the store on the server first in this function:This is a problem however, because when I "stringify" the object, I'll get "Map" tokens or "List" tokens, which won't be readable by the client. Intuitively, I need to convert the state to only consist of plain javascript objects (this will introduce new problems client-side, we'll get to that later). However, the
initialState
object itself is a plain object, but its elements may (or may not!) be immutable objects. Since I also use react-router-redux, therouting
state must be a plain object. My attempted solution involves first applyingfromJS()
to the initialState, and then callingtoJS()
. Again, I wonder how expensive this is, if I have have a large app state tree, and I'm doing this for every request (which could be necessary, because different users may have different initialStates based on localStorage, api calls, etc.). The result looks like this:My question: is there a better way to do this?
Okay, let's say this is okay and continue. The client now boots up the initialState from
window.__INITIAL_STATE__
and sends that intocreateStore()
for the initial app state. But an error will occur about an "unexpected type," because the store is expecting Immutable type objects to hydrate the state, and I sent it plain JS objects (I can't seem to replicate this error anymore, all I get is some internal react error). I cannot simply applyfromJS()
onwindow.__INITIAL_STATE__
since the initialState itself needs to be a plain object, while the elements are Immutable objects. In addition, therouting
field needs to remain a plain object for react-router-redux.My current solution is to abuse
fromJS()
in the reducer generator:This is the more or less working stage of my current project. Sorry for the monstrous post, looking forward to hearing thoughts from the experts!