reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.9k stars 15.27k forks source link

Immutable.js usage - Reducers & Server Side Rendering #1555

Closed sunyang713 closed 8 years ago

sunyang713 commented 8 years ago

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.

import { INCREMENT, DECREMENT } from 'constants'
import Immutable from 'immutable'

const initialState = Immutable.Map({
  value: 0
})

function counter(state = initialState, action) {
  switch (action.type) {
  case INCREMENT:
    return state.update('value', value => value + 1)
  case DECREMENT:
    return state.update('value', value => value - 1)
  default:
    return 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:

import { fromJS } from 'immutable'

function createReducer(initialState, handlers) {
  return function reducer(state = fromJS(initialState), action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}

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:

const initialState = {
  value: 0
}

const counter = createReducer(initialState, {
  [INCREMENT]: state => (
    state.update('value', value => value + 1)
  ),
  [DECREMENT]: state => (
    state.update('value', value => value - 1)
  )
})

Now, I'm ready to connect a react-component to the store. Two high-level questions at this point:

  1. Should the prop(s) connect injects into the component remain an Immutable object, or should it be converted via .toJS()? Or maybe, should the createReducer() helper function return the state.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, if toJS() is not expensive, when I write the mapStateToProps() function for connect(), (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.
  2. Is it okay to be lazy and simply use 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:

function renderFullPage(html, initialState) {
  return `
    <!doctype html>
      ...
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
        </script>
      ...
    `
}

The initialState parameter comes from creating the store on the server first in this function:

function handleRender(req, res) {
  // Create a new Redux store instance
  const store = createStore(counterApp)

  ...

  // Grab the initial state from our Redux store
  const initialState = store.getState() // this line!

  // Send the rendered page back to the client
  res.send(renderFullPage(html, initialState))
}

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, the routing state must be a plain object. My attempted solution involves first applying fromJS() to the initialState, and then calling toJS(). 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:

  const initialState = fromJS(store.getState()).toJS()

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 into createStore() 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 apply fromJS()on window.__INITIAL_STATE__ since the initialState itself needs to be a plain object, while the elements are Immutable objects. In addition, the routing field needs to remain a plain object for react-router-redux.

My current solution is to abuse fromJS() in the reducer generator:

function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](fromJS(state), action)
    } else {
      return fromJS(state)
    }
  }
}

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!

sompylasar commented 8 years ago

Review your data flow and conversions between immutable and js.

  1. Empty state server-side (js)
  2. Reducer server-side (immutable, via createReducer default arg)
  3. Reducer output server-side (immutable, returned from action-wise reducer)
  4. Redux store server-side (immutable, returned by the wrapping function created by createReducer)
  5. store.getState() server-side (immutable, returned from Redux store)
  6. INITIAL_STATE server-side (should be js, but you don't convert!)
  7. INITIAL_STATE browser-side (should be js, but you obviously serialized the immutable and got Map and List etc) ...
sunyang713 commented 8 years ago

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!

sunyang713 commented 8 years ago

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!

alvaromb commented 8 years ago

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?

sunyang713 commented 8 years ago

@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.

alvaromb commented 8 years ago

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.

cchamberlain commented 8 years ago

@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.

cchamberlain commented 8 years ago

@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.

hburrows commented 8 years ago

@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.