Using Immutable in React + React-Redux

React-Redux Introduction

React-Redux is a library for React based on Redux package. And the core idea of React-Redux is to separate the state from the pure components, thereby achieving the purpose of centralized management.html

For example, there is a react component we can call a pure component in Counter.js like this:node

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from  "react" ;
 
export  default  class Counter extends React.Component {
     render(){
         const { count, onIncreaseClick } =  this .props;
         return  (
             <div>
                 <span>current count is: {count}</span>
                 <button onClick={onIncreaseClick}>Increase</button>
             </div>
         );
     }
}

If we want to use React-Redux to control state that mean the props above, we can do like this:react

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {connect} from  "react-redux" ;
import Counter from  './Counter' ;
 
function  mapStateToProps(state)  {
     return  {
         count: state.count
     };
}
 
function  mapDispatchToProps(dispatch) {
     return  {
         onIncreaseClick:  function  () {
             dispatch({type:  'increateCount' });
         }
     };
}
 
export  default  connect(
     mapStateToProps,
     mapDispatchToProps
)(Counter);

You see, we use connect method from react-redux library, and then pass 2 methods that are mapStateToProps and mapDispatchToProps to connect method, when run connect(), it return a method for wrapping a react component like Counter. So, through the above operation, it manages our component - Counter.web

But, there are two questions that what the connect method do for our component and how to manage the react component's props.redux

Don't worry, wait a minute, we will study it together.react-router

What do the Connect Method

connect's main function one is to repackage the two methods passed in that are mapStateToProps and mapDispatchToProps, we can see the main source code:app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction,
// this function wraps mapToProps in a proxy function which does several things:
//
// * Detects whether the mapToProps function being called depends on props, which
// is used by selectorFactory to decide if it should reinvoke on props changes.
//
// * On first call, handles mapToProps if returns another function, and treats that
// new function as the true mapToProps for subsequent calls.
//
// * On first call, verifies the first result is a plain object, in order to warn
// the developer that their mapToProps function is not returning a valid result.
//
export  function  wrapMapToPropsFunc(mapToProps, methodName) {
     return  function  initProxySelector(dispatch, { displayName }) {
         const proxy =  function  mapToPropsProxy(stateOrDispatch, ownProps) {
             return  proxy.dependsOnOwnProps
                 ? proxy.mapToProps(stateOrDispatch, ownProps)
                 : proxy.mapToProps(stateOrDispatch)
         }
 
         // allow detectFactoryAndVerify to get ownProps
         proxy.dependsOnOwnProps =  true
 
         proxy.mapToProps =  function  detectFactoryAndVerify(stateOrDispatch, ownProps) {
             proxy.mapToProps = mapToProps
             proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
             let props = proxy(stateOrDispatch, ownProps)
 
             if  ( typeof  props ===  'function' ) {
                 proxy.mapToProps = props
                 proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
                 props = proxy(stateOrDispatch, ownProps)
             }
 
             return  props
         }
 
         return  proxy
     }
}

Another function is to wrap the component we passed in, we can call the function is High-order components(HOC).dom

A simple HOC such as:ide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import hoistStatics from  'hoist-non-react-statics'
import { Component, createElement } from  'react'
 
export  default  function  wrapWithConnect(WrappedComponent) {
     class HOC extends Component {
         render() {
             const newProps = {
                 id:  '1'
             };
 
             return  createElement(WrappedComponent, {
                 ... this .props,
                 ...newProps
             });
         }
     }
     return  hoistStatics(HOC, WrappedComponent);
}

Why connect need the HOC?oop

Because it want to manage the pure react component such as Counter. 

By using the Redux library, when the state(componets' props) changed, each new component that is wrapped by Connect will compare its own props, whether it is worth updating. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function  makeSelectorStateful(sourceSelector, store) {
     // wrap the selector in an object that tracks its results between runs.
     const selector = {
         run:  function  runComponentSelector(props) {
             try  {
                 const nextProps = sourceSelector(store.getState(), props)
                 if  (nextProps !== selector.props || selector.error) {
                     selector.shouldComponentUpdate =  true
                     selector.props = nextProps
                     selector.error =  null
                 }
             catch  (error) {
                 selector.shouldComponentUpdate =  true
                 selector.error = error
             }
         }
     };
     return  selector
}

If it should update, then this component wrapped of connect will run the setState wrapped with Connect to re-render the real component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
initSubscription() {
     if  (!shouldHandleStateChanges)  return
 
     // parentSub's source should match where store came from: props vs. context. A component
     // connected to the store via props shouldn't use subscription from context, or vice versa.
     const parentSub = ( this .propsMode ?  this .props :  this .context)[subscriptionKey]
     this .subscription =  new  Subscription( this .store, parentSub,  this .onStateChange.bind( this ))
 
     // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
     // the middle of the notification loop, where `this.subscription` will then be null. An
     // extra null check every change can be avoided by copying the method onto `this` and then
     // replacing it with a no-op on unmount. This can probably be avoided if Subscription's
     // listeners logic is changed to not call listeners that have been unsubscribed in the
     // middle of the notification loop.
     this .notifyNestedSubs =  this .subscription.notifyNestedSubs.bind( this .subscription)
}
 
onStateChange() {
     this .selector.run( this .props)
 
     if  (! this .selector.shouldComponentUpdate) {
         this .notifyNestedSubs()
     else  {
         this .componentDidUpdate =  this .notifyNestedSubsOnComponentDidUpdate
         this .setState(dummyState) // dummyState always equal empty object
     }
}

Obviously, if we use the React-Redux framework to manage pure react components, we don't have to execute the shouldUpdateComponent method in those pure react components.

But how to centrally monitor the state of the state changed? 

That's what Redux is responsible for, the HOC of Connect just integrate Redux.

What is Redux

The main idea of Redux is that the web application is a state machine, and the view and state are one-to-one and all states are stored in an object.

we can see a simple Redux is this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const createStore = (reducer, initState = {}) => {
     let state = initState;
     let listeners = [];
 
     const getState = () => state;
 
     const dispatch = (action) => {
         state = reducer(state, action);
         listeners.forEach(listener => listener());
     };
 
     const subscribe = (listener) => {
         listeners.push(listener);
         return  () => {
             listeners = listeners.filter(l => l !== listener);
         }
     };
 
     dispatch({});
 
     return  { getState, dispatch, subscribe };
};

And in React-Redux, it support a Provider component, that we can pass the store ( =createStore() ) to Provider component in top level so that each Component wrapped by connect component can visit the store that created by the Redux, such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from  'react' ;
import ReactDOM from  'react-dom' ;
import { createStore } from  'redux' ;
import { Provider } from  'react-redux' ;
import Counter from  './Counter' ;
 
function  reducerDemo (state, action) {
     switch  (action.type) {
         case  'increase' :
             return  {count: state.count + 1};
         default :
             return  state;
     }
}
let store = createStore(reducerDemo, {count: 1});
 
ReactDOM.render(
     <Provider store={store}>
         <Counter/>
     </Provider>,
     document.getElementById( 'root' )
);

Provider component just do one thing that use the context function, pass the store created by Redux to each Component wrapped with Connect.

We can look at the official document on the introduction of Context:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.

Context more details see here https://reactjs.org/docs/context.html (the latest version).

Note: after 16.3 version, React change the Context usage and the Provider of React-Redux use Context before 16.3 version so you can get it click here.

We get the React-Redux how to manage the whole UI project above and get how to update the component wrapped by connect – each component wrapped by connect subscribe Store, when store triggered by store.dispatch, store will run all method subscribed, then each component will compare itself props with store.getState(), if it judge its props changed, it will trigger its setState method, this is a point that JavaScript's Object is a reference type, even if its children changed, its reference is not changed, React-Redux will think it don't change, so we will improve this.

In normally use, we can easily use Spread (...) or Object.assign to copy a Object, but they are shallow copy and if we encapsulate a method for deep copy, we will deep equal in shouldUpdateComponent.

So we should optimize these  what Redux is responsible for.

Immutable Introduction

We can get its main idea from its name that it make the Object immutable.

In React-Redux project, if we don't use the library such as Immutable, our code like this:

1
2
3
4
5
6
7
8
9
10
11
export  function  selectTreeItem (selectedTreeItem = {}, action) {
   switch  (action.type) {
     case  SELECT_CONFIG_TREE_SERVICE:
       let newState = Object.assign({}, selectedTreeItem);
       newState.service = action.serviceName;
       delete  newState.key;
       return  newState;
     default :
       return  selectedTreeItem;
   }
}

So cumbersome code above ! 

How about optimizing these codes used Immutable:

1
2
3
4
5
6
7
8
9
10
export  function  selectTreeItem (selectedTreeItem = Map(), action) {
   switch  (action.type) {
     case  SELECT_CONFIG_TREE_SERVICE:
       return  selectedTreeItem.set('service', action.serviceName).delete('key');
     default :
       return  selectedTreeItem;
   }
}

Obviously, it doesn't have to worry about references and is more readable.

Another advantage that performance improvement.

Since immutable internally uses the Trie data structure for storage, the values are the same as long as the hashCodes of the two objects are equal. Such an algorithm avoids deep traversal comparisons and performs very well. This is very useful for our performance optimization during the specific rendering process.

And It also has a shortcoming that is very contagious and is used throughout the project we recommended. Because somewhere we use Immutable somewhere, somewhere we use plain Object, we need operate it with plain Object by Immutable.toJS() that is bad action for performance.

How to Integrate Immutable with React-Redux

First we should use redux-immutable library instead of Immutable library in React-Redux project.

And then createStore from Redux need 2 params: reduces and initialState, so we should convert them to Immutable type, such as:

1
2
3
4
5
6
7
8
9
10
11
12
import Immutable from  'immutable' ;
import { combineReducers } from  'redux-immutable' ;
 
const rootReducer = combineReducers(
     {
         routing: routerReducer,
         uiStates: uiStatesReducer
     }
);
 
const initialState = Immutable.Map();
const store = createStore(rootReducer, initialState);

If you don't pass initialState, redux-immutable will also help you build a global Map as the global state with the initial value of each child reducer when the store is initialized. Of course, this requires that each of your child reducer's default initial values be immutable.

Next, you will find that the access to the react-router-redux is also modified because the routerReducer is not compatible with immutable, so you must customize the reducer:

1
2
3
4
5
6
7
8
9
10
11
12
import Immutable from  "immutable" ;
import { LOCATION_CHANGE } from  'react-router-redux' ;
 
const initialState = Immutable.fromJS({
     locationBeforeTransitions:  null
});
const routerReducer = (state = initialState, action) => {
     if  (action.type === LOCATION_CHANGE) {
         return  state.set( 'locationBeforeTransitions' , action.payload);
     }
     return  state;
};

In addition, let the react-router-redux access the routing information that is mounted on the global state:

1
2
3
4
5
6
7
import {hashHistory} from  'react-router' ;
import { syncHistoryWithStore } from  'react-router-redux' ;
const history = syncHistoryWithStore(hashHistory, store, {
     selectLocationState (state) {
         return  state.get( 'routing' ).toObject();
     }
});

Finally, we change our pure components' props validate by 'react-immutable-proptypes', such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from  'react' ;
import ImmutablePropTypes from  'react-immutable-proptypes' ;
import PropTypes from  'prop-types' ;
 
class App extends Component {
     render() {
         return  <div></div>
     }
}
 
App.propTypes = {
     children: PropTypes.node,
     errors: ImmutablePropTypes.list,
     currentUser: ImmutablePropTypes.map
};
 
export  default  App;

And change the container wrapped the pure component, such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { connect } from  'react-redux' ;
import App from  './App' ;
 
function  mapStateToProps (state) {
     return  {
         currentUser: state.get( 'currentUser' ),
         errors: state.get( 'errors' )
     }
}
 
function  mapDispatchToProps (dispatch) {
     return  {
         actions: ()  => dispatch({})
     }
}
 
export  default  connect(mapStateToProps, mapDispatchToProps)(App);

That's all, thanks.

Other article about Immutable+Rudux

相關文章
相關標籤/搜索