When to use .toJS() with Immutable.js and Flux?
Asked Answered
C

5

45

I'm trying to use ImmutableJS with my React / Flux application.

My stores are Immutable.Map objects.

I'm wondering at which point should I use .toJS() ? Should it be when the store's .get(id) returns ? or in the components with .get('member') ?

Candiecandied answered 9/1, 2015 at 16:13 Comment(2)
Good question. I Wouldn't do it in the store though, since then you lose the abillity to do a simple object comparison (prevState !== this.state) if you'd want to optimize rendering with shouldComponentUpdate.Fabiolafabiolas
Thanks, indeed that's a good point to not use toJS() in the stores.Candiecandied
M
46

Ideally, never!

If your Flux stores are using Immutable.js then try to maintain all the way through. Use React.addons.ReactComponentWithPureRenderMixin to achieve a memoization performance win (it adds a shouldComponentUpdate methods).

When rendering, you may need to call toJS() as React v0.12.x only accepts Array as children:

render: function () {
  return (
    <div>
      {this.props.myImmutable.map(function (item) {
        <div>{item.title}</div>
      }).toJS()}
    </div>
  );
}

This have changed in React v0.13.x. Components accept any Iterable as children instead of only Array. Since Immutable.js implements Iterable, you are able to omit the toJS():

render: function () {
  return (
    <div>
      {this.props.myImmutable.map(function (item) {
        <div>{item.title}</div>
      })}
    </div>
  );
}
Mig answered 21/1, 2015 at 6:4 Comment(6)
Lee, do you have any advice for proptypes? I'm used to using arrayOf(shape...).Ocam
now if only my external libraries also supported Immutable.jsForsake
So, are we saying that using the read API (get(), getIn()) from Immutable.js in React components is the right thing to do? This feels like leaky abstraction and means that if I ever change how I maintain the state in my stores, I have to touch every component that reads a value. Something feels wrong about this...Rask
@Rask the whole point of Immutable.js is to have your state and prop values be Immutable.js objects, and then to implement the PureRenderMixin. If you call toJS() before the props/state hit your component, then you lose the ability of PureRenderMixing to compare your objects by reference, which is the core idea behind using Immutable objects for React components with the PureRenderMixin.Morion
@MatthewHerbst, I wasn't actually suggesting the use of toJS(), which has the same inherent problem to me: leaking a design decision, that would otherwise be confined completely to the Stores, into the rest or your application. I get that the inability to use the conventional property accessors is due to a limitation of Immutable.js implementation; I'm just pointing out that this is a significant limitation, unless you are ok with exposing a library to the components of your application, which otherwise have know business being aware of the library.Rask
I'm studying the following example that uses toJS() for local storage. Do you think that's a legitimate use or is there a better way? github.com/e-schultz/ng2-camp-example/blob/master/src/store/…Endermic
B
5

Kinda old question but lately I've been experimenting with this approach using reselect and lodash's memoize in the effort of returning comparable objects to React's Components.

Imagine you have a store like this:

import { List, Map } from 'immutable';
import { createSelector } from 'reselect';
import _ from 'lodash'; 

const store = {
    todos: List.of(
        Map({ id: 1, text: 'wake up', completed: false }), 
        Map({ id: 2, text: 'breakfast', completed: false })
    )
};

const todosSelector = state => state.todos;

function normalizeTodo(todo) {
    // ... do someting with todo
    return todo.toJS();
}

const memoizeTodo = _.memoize(normalizeTodo);

export const getTodos = createSelector(
    todosSelector,
    todos => todos.map(memoizeTodo)
);

Then I pass to a TodoList component todos as a prop, which will then be mapped into two TodoItem Components:

class TodoList extends React.Component {
    shouldComponentUpdate(nextProps) {
         return this.props.todos !== nextProps.todos;
    }

    render() {
       return (<div>
           {this.props.todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
       </div>);
    }
}

class TodoItem extends React.Component {
    shouldComponentUpdate(nextProps) {
        return this.props.todo !== nextProps.todo;
    }

    // ...
}

This way, if nothing changed in the todo's store, when I call getTodos() reselect returns to me the same object, and so nothing gets re-rendered.

If, for example, todo with id 2 is marked as completed, it also changes in the store, and so a new object is returned by todosSelector. Then the todos are mapped by memoizeTodo function, which should return the same object if a todo isn't changed (since they are Immutable Maps). So when TodoList receives the new props, it re-renders because todos has changed, but only the second TodoItem re-renders because the object representing the todo with id 1 didn't change.

This surely could lead to a performance loss, especially if our store contains a lot of items, but I didn't notice any problem in my medium-size app. The upside of this approach is that your Components receives plain javascript objects as props and could use them with something like PureRenderMixin, so how objects are returned from the store is not business of Components anymore.

Belgrade answered 20/1, 2016 at 9:30 Comment(1)
amazing solution !Spline
R
3

Like @LeeByron said, you shouldn't have to call a toJS. Under React 0.14.*, calling map on an Immutable Map will work and render correctly, but you will end-up with a warning:

Using Maps as children is not yet fully supported. It is an experimental feature that might be removed. Convert it to a sequence / iterable of keyed ReactElements instead.

To deal with this, you can call toArray() on your Map like:

render () {
  return (
    <div>
      {this.props.immutableMap.toArray().map(item => {
        <div>{item.title}</div>
      })}
    </div>
  )
}

Converting your iterable to an array and giving React what it wants.

Rapid answered 9/3, 2016 at 0:48 Comment(1)
Thanks for your answer been looking around for so long!Wesla
E
2

Good point raised by @Hummlas.

I personnally use it in my React components, when I iterate over a collections to render an array of sub components:

this.props.myImmutableCollection.map(function(item, index) {
    React.DOM.div null, item.get('title');
}).toJS();

If you don't use .toJS(), React won't recognize the mapped elements as an array of components.

Ethiopia answered 12/1, 2015 at 16:26 Comment(0)
H
0

-- No longer recommend --
When using Redux I tend to let my connects' mapStateToProps function transform immutable structures using toJS() and allow my react components to consume the props as javascript objects.

Heaton answered 4/1, 2016 at 1:58 Comment(5)
But toJS will return a new deeplyclone object. It not good for performace.Petronella
That's true, but using something like reselect you could return a new object only if the underlying Immutable one has changed. The only problem is with nested data. For example, if only one element of a List of Maps did change, a whole new array with new objects will be returned, triggering a render in each child view. With immutable only the child that did change would be re-rendered...Belgrade
I have stopped doing what I suggested because of performance.Heaton
@WalkerRandolphSmith so what are you doing now? I'm encountering the same problem and came to the same solution.Munt
I use Immutable Records so the interface (dot notation) does not change from a js object.Heaton

© 2022 - 2024 — McMap. All rights reserved.