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 is awesome library that can help you solve this problem.
Let’s first learn what was Reselect built for:
Simple “selector” library for Redux.
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:
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