How to use ImmutableJS without going crazy

Written by svitekpavel | Published 2018/03/16
Tech Story Tags: react | redux | immutable | programming

TLDRvia the TL;DR App

If you are in the Javascript ecosystem you probably know that there are a lot of new libraries and good practices are still being developed, especially in ReactJS ecosystem.

If you are developing Single page applications using ReactJS and you are using Redux for state management, you have probably heard about ImmutableJS.

It’s a library that helps developers keep their state immutable to avoid bugs that are hard to find.

Not even that but it also helps React re-render components in the right time. You probably know that React uses shallow comparison to know if some of the props have changed.

This could introduce problems when you are passing structured data into your components (arrays, objects). React don’t perform deep comparison (because it’s expensive), so you may run into unexpected behavior.

// In reducer.jsconst state = {usersPage: {loading: false,isFetched: false,list: [],},companiesPage: {loading: false,isFetched: false,list: [],},};

// In UsersPage.jsclass UsersPage extends React.Component {render() {const { loading, list } = this.props.usersPage;

return (  
  <div>  
    { loading && <LoadingIndicator /> }  
    { !loading && <UsersTable list={list} /> }  
  </div>  
);  

}}

function mapStateToProps(state) {return {usersPage: selectors.selectUsersPage(state),};}

// In selectors.jsconst selectUsersPage = (state) => state.usersPage;

Considering code above, all seems fine. However, if you implement updating a state in a wrong way, eg. you don’t create a new object, React won’t update your component. Eg:

// Incorrect implementationfunction reducer(state, action) {switch(action.type) {case 'USERS_LOADED':state.usersPage.list = action.payload;state.usersPage.loading = false;state.usersPage.isFetched = true;return state;default:return state;}}// Correct implementationfunction reducer(state, action) {switch(action.type) {case 'USERS_LOADED':return {...state,usersPage: Object.assign({}, state.usersPage, {list: Object.payload,loading: false,isFetched: true,}),};default:return state;}}

As you can see in the code above, things can get tricky really easily. You have to make sure, you are returning new object while you are preserving other branches of the state and updating variables in current branch correctly.

Now, let’s look at implementation in ImmutableJS:

import { fromJS } from 'immutable';const state = fromJS({usersPage: {loading: false,isFetched: false,list: [],},companiesPage: {loading: false,isFetched: false,list: [],},});

function reducer(state, action) {switch(action.type) {case 'USERS_LOADED':return state.setIn(['usersPage', 'list'], fromJS(action.payload)).setIn(['usersPage', 'isFetched'], true).setIn(['usersPage', 'loading'], false) ;default:return state;}}

This syntax make things much more simple to read and maintain. And ImmutableJS returns a new object every time when you change something in the state.

However, you will have to use ImmutableJS getters to get to the values:

// In UsersPage.jsclass UsersPage extends React.Component {render() {const { usersPage } = this.props;const loading = usersPage.get('loading');const list = usersPage.get('list').toJS();

return (  
  <div>  
    { loading && <LoadingIndicator /> }  
    { !loading && <UsersTable list={list} /> }  
  </div>  
);  

}}

function mapStateToProps(state) {return {usersPage: selectors.selectUsersPage(state),};}

// In selectors.jsconst selectUsersPage = (state) => state.get('usersPage');

What’s even worse, sometimes ImmutableJS objects will leak deeper into your components, maybe even to your presentional components (look at the toJS()). This is often one of the reasons, why people argue against using ImmutableJS.

It gets even worse when you are planning to publish your package. You don’t want to tie hands of developers using it by having them to use certain library.

So how to combine best of both worlds?

Reselect to the rescue!

Reselect is awesome library that can help you solve this problem.

Let’s first learn what was Reselect built for:

Simple “selector” library for Redux.

  • Selectors can compute derived data, allowing Redux to store the minimal possible state.
  • Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
  • Selectors are composable. They can be used as input to other selectors.

Bare with me for a minute. Consider this code:

import { createSelector } from 'reselect';

const selectUsersPageDomain = (state) => state.get('usersPage');

const makeSelectUsersPage = createSelector(selectUsersPageDomain,(substate) => substate.toJS(),);

Then, you can use again the pure JS syntax again:

import { createStructuredSelector } from 'reselect';

class UsersPage extends React.Component {render() {const { loading, list } = this.props.usersPage;

return (<div>{ loading && <LoadingIndicator /> }{ !loading && <UsersTable list={list} /> }</div>);}}

const mapStateToProps = createStructuredSelector({usersPage: selectors.makeSelectUsersPage(),});

If nothing changes in the userPage branch, the selector doesn’t recompute its value and React won’t trigger component render.

As you can see, using this strategy, you can combine best of both worlds.

But there’s one important thing that is not obvious benefit I haven’t talked about:

When you are updating some branch using ImmutableJS, all untouched branches remain the same:

const { fromJS } = require('immutable');const state = fromJS({usersPage: {loading: false,isFetched: false,list: [],},companiesPage: {loading: false,isFetched: false,list: [],},});

console.log('Updating usersPage branch');const state1 = state.setIn(['usersPage', 'loading'], true);state.get('usersPage') === state1.get('usersPage'); // false**state.get('companiesPage') === state1.get('companiesPage'); // true**state === state1; // false

console.log('Updating companiesPage branch');const state2 = state1.setIn(['companiesPage', 'loading'], true);**state1.get('usersPage') === state2.get('usersPage'); // true**state1.get('companiesPage') === state2.get('companiesPage'); // falsestate1 === state2; // false

Note: I removed _console.log_ statements to make the lines shorter.

If you look at the code, you will see that branches which are not updated will stay the same -> so reselect selectors will not trigger update. Only selectors of updated branches will be updated.

But the root of the state is updated as well! So you have to be a bit careful when writing your selectors:

// In reducer.jsconst { fromJS } = require('immutable');const state = fromJS({pages: { // We put our pages 1 more level deeper usersPage: {loading: false,isFetched: false,list: [],},companiesPage: {loading: false,isFetched: false,list: [],},}});

// In selectors.js**// Incorrect implementation**import { createSelector } from 'reselect';**const selectPagesDomain = (state) => state.get('pages');**const selectUsersPageDomain = (state) => state.get('usersPage');const selectCompaniesPageDomain = (state) => state.get('companiesPage');

const makeSelectUsersPage = createSelector(selectPagesDomain, // source of problemselectUsersPageDomain,(substate) => substate.toJS(),);

const makeSelectCompaniesPage = createSelector(selectPagesDomain, // source of problemselectCompaniesPageDomain,(substate) => substate.toJS(),);

If you implement the selectors like this, both makeSelectCompaniesPage and makeSelectUsersPage will be triggered when you change value only in one of them. This is probably something you DON’T want.

Why are they triggered? Look back at this rule:

  • Selectors are efficient. A selector is not recomputed unless one of its arguments changes.

Since selectPagesDomain will return different value if any of its branches has changed, reselect will recompute both selectors every time.

This is the correct implementation:

// Correct implementationimport { createSelector } from 'reselect';const selectUsersPageDomain = (state) => state.getIn(['pages', 'usersPage']);const selectCompaniesPageDomain = (state) => state.getIn(['pages', 'companiesPage']);

const makeSelectUsersPage = createSelector(selectUsersPageDomain,(substate) => substate.toJS(),);

const makeSelectCompaniesPage = createSelector(selectCompaniesPageDomain,(substate) => substate.toJS(),);

You want to select the path that is closest to the data you want to use in your selector to prevent recomputing when other branches are changed.

If you made it till here, congratulations!

Let’s put it all together:

**// In reducer.js**const { fromJS } = require('immutable');const state = fromJS({pages: {usersPage: {loading: false,isFetched: false,list: [],},companiesPage: {loading: false,isFetched: false,list: [],},}});

function reducer(state, action) {switch(action.type) {case 'USERS_LOADED':return state.setIn(['usersPage', 'list'], fromJS(action.payload)).setIn(['usersPage', 'isFetched'], true).setIn(['usersPage', 'loading'], false);case 'COMPANIES_LOADED':return state.setIn(['companiesPage', 'list'], fromJS(action.payload)).setIn(['companiesPage', 'isFetched'], true).setIn(['companiesPage', 'loading'], false);default:return state;}}

export default reducer;

**// In selectors.js**import { createSelector } from 'reselect';const selectUsersPageDomain = (state) => state.getIn(['pages', 'usersPage']);const selectCompaniesPageDomain = (state) => state.getIn(['pages', 'companiesPage']);

export const makeSelectUsersPage = createSelector(selectUsersPageDomain,(substate) => substate.toJS(),);

export const makeSelectCompaniesPage = createSelector(selectCompaniesPageDomain,(substate) => substate.toJS(),);

**// In UsersPage.js**import { createStructuredSelector } from 'reselect';import * as selectors from './selectors';

class UsersPage extends React.Component {render() {const { loading, list } = this.props.usersPage;

return (  
  <div>  
    { loading && <LoadingIndicator /> }  
    { !loading && <UsersTable list={list} /> }  
  </div>  
);  

}}

const mapStateToProps = createStructuredSelector({usersPage: selectors.makeSelectUsersPage(),});

export default connect(mapStateToProps)(UsersPage);

**// In CompaniesPage.js**import { createStructuredSelector } from 'reselect';import * as selectors from './selectors';

class CompaniesPage extends React.Component {render() {const { loading, list } = this.props.companiesPage;

return (  
  <div>  
    { loading && <LoadingIndicator /> }  
    { !loading && <CompaniesTable list={list} /> }  
  </div>  
);  

}}

const mapStateToProps = createStructuredSelector({companiesPage: selectors.makeSelectCompaniesPage(),});

export default connect(mapStateToProps)(CompaniesPage);

That’s it!

Now you can use all the advantages of both ImmutableJS without the need to use its getters in your containers or presentional components.

On top of that, you gained strengths of reselect library, which can help you make your selectors more efficient.

Let me know what do you think!

References:

Redux_If you don't use a module bundler, it's also fine. The npm package includes precompiled production and development…_redux.js.org

reactjs/reselect_reselect - Selector library for Redux_github.com


Published by HackerNoon on 2018/03/16