Redux-form: Immutable store?

Created on 1 Oct 2015  路  48Comments  路  Source: redux-form/redux-form

Hi, we'd like to use redux-form in our react app with redux. In our app, our reducers never mutate state (we use Facebook's immutable.js to achieve that), but I can see that the reducer used in redux-form is just plain javascript object. When we tried to redux-form in our app, we would get mixed data type in state, causing errors like The previous state received by the reducer has unexpected type of "Object".

Based on this Redux issue (https://github.com/rackt/redux/issues/548), Redux itself is made not having the knowledge of type of state one uses, so it does not matter if plain object, immutable object, etc is used for state; certainly one don't want to mix up of types in a state (or the error I mentioned above would occur).

Do you have intention of using immutable Map/List in your reducer? If not, do you have any idea how we can still use redux-form by converting the form state to immutable Map (like using fromJS for conversion)? How do other developers get around this kind of problem?

Thanks,
Alex

Most helpful comment

@jasan-s ImmutableJS _does_ provide a performance and memory boost for large structures. Not so much for small ones. This is why great pains were taken to have Immutable JS support out of the box in v6.

All 48 comments

I'm amazed that it took this long for someone to ask this question.

I prefer the library using plain javascript objects for three reasons:

  1. Fewer dependencies
  2. Less opinionated design
  3. I can't get enough of the ... destructuring operator.

Having only put about a minute of thought into it, it seems like, given the (state, action) => state nature of Redux reducers, it would be pretty trivial to wrap any reducer in another function to make it use Immutable.js. In fact, let me have a go:

import Immutable from 'immutable';
import {reducer as formReducer} from 'redux-form';

const immutableize = reducer => (state, action) =>
  Immutable.fromJS(reducer(state.toJS(), action));

const immutableFormReducer = immutableize(formReducer);

Voila. It's a hack, yes, but it will take an Immutable.Map, let the inner reducer use javascript objects, and return an Immutable.Map.

I suppose that, long term, if demand is high enough, or if I convince myself to use Immutable in my apps that are dogfooding redux-form, there might eventually be a separate version of the redux-form reducer that uses Immutable internally, but there are no such plans at the moment.

Thanks for the quick response.

P.S. If you do try to do like I suggested with the pseudocode, be aware of the initialization edge case, where state.toJS() will fail because state is undefined.

Thanks for the tip. Will keep it in mind.

Alex

I don't use ImmutableJS, but :+1: great question!

I still got the same error after doing what you suggested. Here is how I combine my reducers (reducers such as auth and apps all use immutable Map for state data):

import { combineReducers } from 'redux';
import {fromJS} from 'immutable';

import auth from './auth';
import {reducer as formReducer} from 'redux-form';
import apps from './apps';

const immutableize = reducer => (state, action) =>
  fromJS(reducer(state ? state.toJS() : {}, action));
const form = immutableize(formReducer);

export default combineReducers({
  auth,
  form,
  apps
});

I noticed that I also do bindActionCreators with initializeWithKey in my React component. Will that affect the internal data type of the app's state? What else do I need to change, I wonder?

Thanks,
Alex

initializeWithKey just generates a normal action. You should probably change state ? state.toJS() : {} to state && state.toJS() so it will be undefined when state is undefined.

I use https://github.com/indexiatech/redux-immutablejs to mange most of my state in an immutable fashion but still work with Redux-form, I treat redux-form as a separate component and let it do its thing without interfering, it acts just fine together.

Thanks for that, @asaf.

@asaf, how do you do that when redux-immutablejs's combineReducers creates a Map, and when redux form does a lookup, it uses [] and stuff?

The error I'm seeing when trying to use redux-form with redux-immutablejs is:

Error: You need to mount the redux-form reducer at "form" at redux-form/lib/createHigherOrderComponent.js:334

The problem is that the state object is an Immutable object, and it does the check using []:

if (!state[reduxMountPoint]) {

There must be a way to allow some reducers to not use immutablejs and let the others use it. Probably some combination of Redux's combineReducers and redux-immutablejs's combineReducers.

For now, redux-form is going to continue to use regular javascript data structures for its state.

@erikras, I think the problem @pheuter has (and that I have too) cannot be solved just by wrapping the reducer, because the problem doesn't happen in the reducer. redux-form/lib/createHigherOrderComponent.js grabs the state directly from redux via react-redux's connect function, and then makes the assumption that the top level state object is a regular JS object. For people who use immutable, the top level state object is not a regular JS object, it's an Immutable.Map

Would you be amenable to a pull request that adds adds a new config option to the @reduxForm() that is a function to get the form object out of the state from wherever it might be and whatever object type it might use? Potentially, this functionality could replace or obsolete reduxMountPoint as well, and would allow for mountPoints that aren't children of the root state object (if, for instance, you only use forms in dialogs and wanted redux-form mounted at state.modalDialogs.form or similar) . @kinseyost was working on something similar to it, and I think it could be useful to others who run into similar issues with immutable and redux-form.

Well put @itaylor, I think the idea around this flexible function to get form object agonstic of type is a good approach to this problem.

That sounds like a very agreeable solution, @itaylor.

Cool, I'll move forward with a PR that adds this then. Expect it in the next few days.

Fixed in v3.0.7.

Sorry, I didn't wait for you, @itaylor. :-)

Thanks guys for your input and work!

I don't really have a setup to verify that it works, so if one of you could report back about how easy it is to accomplish, it would be much appreciated.

@erikras, You beat me to it on the implementation, but you might want to just use the two new tests I added in my fork.

https://github.com/erikras/redux-form/compare/master...itaylor:master

One of them tests fetching from an alternate location in the store, one actually uses immutable and redux-immutablejs to prove that works. If you want me to make a PR with just the tests, I can do that too.

Also, I found that not all the tests were running properly on my machine because the globs were being handled by the shell instead of being passed to mocha. See https://github.com/erikras/redux-form/commit/ee512faca06c1d0df5f43d3af7b7cee25a1d91ef for that as well.

Yep, I made that change for the tests, too. Some Windows user removed those 's a couple weeks ago.

I will check out your tests.

getFormState() is a function which returns the top level state object.

So as I am using redux-immutable I need to fetch this top level state object which is an Immutable.Map. I would then call .toJS() and return the output right?

Something like this:

getFormState: (state, reduxMountPoint) => state[reduxMountPoint].toJS()

@hairychris
If your root state object is an Immutable.Map, and you want to use the reduxMountPoint to indicate where it the state object your form state is, you'd use something like the following:

getFormState: (state, reduxMountPoint) => state.get(reduxMountPoint).toJS()

Wow, that was freaky. I was just reading @hairychris's comment, when @itaylor's comment appeared, right at the same scroll position, effectively fixing the code in place. Yes, what @itaylor said.

Gotcha, makes sense, thanks =)

Without meaning to turn this into StackOverflow how are you handling connecting redux-forms reducers when using combineReducers from redux-immutable?

Something along the lines of:

import { combineReducers } from 'redux';
// redux-utils just wraps redux-immutable
import { combineReducers as combineReducersUtils } from 'redux-utils';
import {reducer as formReducer} from 'redux-form';
import devices from './devices';
import alarms from './alarms';

const mainReducers = combineReducersUtils({
  devices, alarms
});

const extraReducers = combineReducers({
  form: formReducer
});

export default (mainReducers + extraReducers);

Although obviously that doesn't work because I can't just naively concatenate the outputs of those function like that.

@itaylor, can you address @hairychris's concern here? I'm not well versed in using Immutable with Redux.

@hairychris It's might be possible that reduce-reducers will let you do something like that but I'm not sure if it will work with the mix of immutable and plain.

Thanks for bumping this @erikras and thanks for the infor @neverfox I'll give it a go and post here if I get something together.

@hairychris, I think redux-immutable may be too opinionated about how you format your actions/reducers for it to be easily integrated with redux-form. redux-immutablejs might be a better fit for scenarios like this where you're trying to tie multiple types of reducers together, although in my projects I'm not using it either, in favor of my own root reducer. I can probably make a simple example that uses redux-immutablejs and redux-form.

Actually, there's already the start of in the immutablejs test that I added when I was working on PR for this issue: https://github.com/itaylor/redux-form/blob/7be128b73c6d3676c0d93df9b4152f3ef152a317/src/__tests__/immutableJsStore.spec.js

Thanks for that, @itaylor. If you could come up with a short "How To" for doing this, I'd be happy to add it to the official FAQ.

I personally avoid the redux-immutable/immutablejs libraries because they don't play well with 3rd-party reducers (since library authors rightly don't try to force an immutability library on you at the level of the public API). There is much less pain just making your custom reducers work with immutable sub-trees using the library of your choice, leaving the top level as plain JS so that things like redux-form and redux-router don't need special handling or custom selectors.

^^ :+1:

@neverfox I can understand where you're coming from, but why is the solution to avoid using immutable libraries instead of the libraries making fewer assumption about the structure of the top-level state?

There are some great benefits of having the top-level state be an immutable object itself, and having to sacrifice that functionality because certain libraries make type-related assumptions about the state isn't great.

Another way to think about this is if JavaScript was a strongly typed language, Redux would probably use a bounded type for the state, and allow other libraries to extend that functionality without being constrained to particular container types.

I may have been unclear but I didn't mean to suggest not using immutable libraries, e.g. ImmutableJS. I just meant the two libraries redux-immutable and redux-immutablejs. I am genuinely interested to know what advantages you have found to having the top level keys of your state require access via an immutable API? The only think I can think of is that it might speed up memoization checks when utilizing selectors, but I don't tend to have any of my rendering dependent on those top levels.

Sorry, I have misinterpreted what you said about avoiding these libraries in general.

The main advantage is consistency. When writing reselect selectors, you descend from the top-level state. It is awkward to have a selector that accesses state using regular object lookup as well as immutable's get() in the same statement, while a reducer operates on a sub-property that is already an Immutable.Map:

selector = state => state['user'].getIn(['session', 'token']);
createReducer(initialState, {
  'SET_TOKEN': (state, action) => state.setIn(['session', 'token'], action.token)
});

This is a sample advantage that caters more to consistency than to optimization, but that is nonetheless important when building large applications with large teams.

Fair enough, but don't you run into the same thing in reverse, e.g. with redux-router:

selector = state => state.get('router').location.query.q;

Yeah, and we still run into this with redux-form, since the form object still has deeply nested mutable types. At that point you can either let the library handle that manipulation for you (which it does in the case with redux-form), or you can convert between mutable and immutable types on your own.

Since redux-form is written in such a way that we never actually need to write any code around going into the form object, we don't care that it deals with mutable objects since that is abstracted away by the library.

@neverfox thanks for your assistance, having a mutable root node and immutable nodes seems like the best solution.

@hairychris, if you want to see an example of a full immutable store, here's one. https://github.com/itaylor/redux-form-immutablejs-example

Yes, if plugin authors provide hooks for customizing state access, these problems can be worked around. I had no luck with the one form react-router, but then again I switched to react-simple-router anyway for other reasons.

Thanks for that, @itaylor! I've added it to the FAQ.

@erikras, you mentioned you use (...) spread operator in reducers instead of immutable js. Would you than say that a library like Immutable JS does not add much of performance boost in a production app compared to copying entire state using spread operator in reducers?

@jasan-s ImmutableJS _does_ provide a performance and memory boost for large structures. Not so much for small ones. This is why great pains were taken to have Immutable JS support out of the box in v6.

Hey @erikras, does http://redux-form.com/6.0.0-rc.1/examples/initializeFromState/ work with immutable stores?
I can't seem to get this working.

@jaspersorrio Yes. Make sure you import everything from redux-form/immutable. See the Immutable Example for further details.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings