over 3 years ago

範例如下

import React, { Component } from 'react';
import { Motion, spring} from 'react-motion';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      action : true
    };
    this._onAction = this._onAction.bind(this);
    this.handleRest = this.handleRest.bind(this);

  }
  // 執行動畫
  _onAction(e) {
    this.setState({action: !this.state.action});
  }
  // 動畫結束後執行
  handleRest() {
      console.log("here");
  }
  render() {
    // 設定預設 Style
    let defaultStyle = {width: 0, height: 0};
    if (this.state.action) {
      defaultStyle.width = 100;
      defaultStyle.height = 200;
    }
    // 設定動畫的 style 使用 spring 函數來控制樣式的數字變化
    let actionStyle = {width: spring(defaultStyle.width, {stiffness: 267, damping: 13}), height: spring(defaultStyle.height, {stiffness: 267, damping: 13})};
    return (
      <div>
        <input type="button" value="Action" onClick={this._onAction} />
        <Motion ref="motion" onRest={this.handleRest} defaultStyle={defaultStyle} style={actionStyle}>
          {interpolatingStyle => <div  style={{width: interpolatingStyle.width, height : interpolatingStyle.height, background:'black'}} />}
        </Motion>
      </div>
    );
  }
}

export default App;

defaultStyle 可預設初始 style (可不設定, 此範例可直接拿掉執行)
style 配置所要執行的動畫屬性 (必須設定)
兩種 style 的樣式必須為數字型態

spring函數 是用來控制樣式屬性數字的變化
可搭配 stiffnessdamping 來控制數字的增減速度, 來達成搖晃的效果
呼叫方式

// 使用預設的 {stiffness: 170, damping: 26} 
spring(10) 
// 自行指定 stiffness 及 damping 參數
spring(100, {stiffness: 267, damping: 13})
// 使用 presets 來簡化配置 stiffness 及 damping
import { Motion, spring, presets} from 'react-motion';
spring(10, presets.wobbly)

對於 stiffnessdamping 的搖晃程度可參考官方提供的模擬環境
http://chenglou.github.io/react-motion/demos/demo5-spring-parameters-chooser/
presets 的配置值可參考
https://github.com/chenglou/react-motion/blob/9cb90eca20ecf56e77feb816d101a4a9110c7d70/src/presets.js

interpolatingStyle 會依 style 執行的 spring 來動態呈現樣式屬性的數字變化

//直接配置
{interpolatingStyle => <div  style={interpolatingStyle} />}
//呈現格式
<div style="width: 100px; height: 200px;"></div>

//呼叫屬性配置
{interpolatingStyle => <div  style={{width: interpolatingStyle.width, height : interpolatingStyle.height, background:'black'}} />}
//呈現格式
<div style="width: 100px; height: 200px; background: black;"></div>

還有 StaggeredMotion 及 TransitionMotion 供使用
可參考官方文件
https://github.com/chenglou/react-motion

 
over 3 years ago

一般 Component 之間的 event 或 資料會藉由 props 來做傳遞
例如

<UserDetail ref="addUserDetail" actions={this.props.actions} reload={this._reload} />

但如果 Component 之間的階層太過複雜
往往會造成 props 亂飛的窘境

這時需使用事件監聽的模式來控管 Component 之前的 event

此範例使用 PubSubJS 來訂閱一個監聽的 event 給各個子 Component 使用

請先

npm install pubsub-js

使用範例

import React, {Component} from 'react';
import pubsub from 'pubsub-js';

class Main extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
            <ProductSelection />
            <Product name="product 1" />
            <Product name="product 2" />
            <Product name="product 3" />
            </div>
        );
    }
}

class ProductSelection extends Component {
  constructor(props) {
      super(props);
      this.state = {
        selection: 'none'
      }
  }
  componentDidMount() {
    // 訂閱一個監聽事件來 set State
    this.pubsub_token = pubsub.subscribe('products', function(topic, product) {
      this.setState({ selection: product });
    }.bind(this));
  }
  componentWillUnmount() {
    // 使用 subscribe 產生的 this.pubsub_token 來取消 監聽事件
    pubsub.unsubscribe(this.pubsub_token);
  }
  render() {
    return (<div>You have selected the product : {this.state.selection}</div>);
  }
}

class Product extends Component {
  constructor(props) {
      super(props);
      this.onclick = this.onclick.bind(this);
  }
  onclick() {
    // 呼叫 'products' 監聽事件
    pubsub.publish('products', this.props.name);
  }
  render() {
    return(
      <div onClick={this.onclick}>{this.props.name}</div>
    );
  }
}

export default Main;

當 Product 執行 onclick() 時會觸發 ProductSelection 內所訂閱的 products 事件
來改變 ProductSelection 的 selection 狀態

更進階的用法可參考 PubSubJS

參考來源
https://ctheu.com/2015/02/12/how-to-communicate-between-react-components/

 
over 3 years ago

react-i18next 是使用 i18next 所建立的 Higher-order components 和 components

於 package.json 建立及安裝底下套件

"i18next": "3.3.1",
"i18next-browser-languagedetector": "0.3.0",
"i18next-xhr-backend": "0.6.0",
"react-i18next": "^1.6.0",

建立 i18n.js

import i18n from 'i18next';
import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';


i18n
  .use(XHR)
  .use(LanguageDetector) // 偵測瀏覽器語系
  .init({
    fallbackLng: 'en', // 未偵測到時的後備語系
    ns: ['common'], // 語系的 loading namespace 如語系檔案名稱 common.js
    defaultNS: 'common', // 預設的 namespace name
    debug: true,
    interpolation: {
      escapeValue: false // not needed for react!!
    },
    backend: {
    // 設定語系檔案的 server 路徑, 會以GET的方式跟 server 要檔案
    // lng = 語系代碼 ns = namespace
    "loadPath": "/javascripts/locales/{{lng}}/{{ns}}.json"
    }
  });


export default i18n;

根據上面的 loadPath 路徑於server端建立語系檔案
javascripts/locales/en/common.json

{
    // 需使用 "
  "appName": "react-i18next-example" 
}

javascripts/locales/zh-TW/common.json

{
  "appName": "魔鬼精肉人"
}

配置 i18n.js
底下範例是搭配 react-redux 跟 react-router

...
..
.
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';

// 可使用 changeLanguage 強制轉換語系
//i18n.changeLanguage('zh-TW');
render(
  <Provider store={store}>
      <I18nextProvider i18n={ i18n }>
            <Router history={history} routes={PageRoutes} />
      </I18nextProvider>
  </Provider>
  ,
  reactElement
);

與自己的 components 整合

import React, {Component} from 'react';
import { translate } from 'react-i18next';

class Main extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        const { t } = this.props;
        return (
            <div>{t('appName')}</div>
        );
    }
}

// 使用 react-i18next 的 HOC translate
export default translate()(Main); // 會採用預設的 defaultNS : common 語系檔案 common.json
// 也可以指定到哪個語系檔案 底下範例為 view.json 
//export default translate('view', { wait: true })(Main);

語系代碼會配置到瀏覽器 Local Storage key 為 i18nextLng 下
可於 chrome 開發人員工具下的 Resources 頁籤查看

 
over 3 years ago

pageRoutes.js

// 此範例是檢查 localStorage.token 來決定要不要導入登入頁面
// 如條件成立則利用 replace 導入所要的路徑

function  enterRedirectToLogin(nextState, replace) {
  if (!localStorage.token) {
    replace({
      pathname: '/Login',
      state: { nextPathname: nextState.location.pathname }
    });
  }
}

function changeRedirectToLogin(prevState, nextState, replace) {
  if (!localStorage.token) {
    replace({
      pathname: '/Login',
      state: { nextPathname: nextState.location.pathname }
    });
  }
}

function redirectToMain(nextState, replace) {
  if (localStorage.token) {
    replace({
      pathname: '/',
      state: { nextPathname: nextState.location.pathname }
    });
  }
}

export default {
  childRoutes: [
  {
    // 使用 onEnter 直接導入底下 childRoutes path 時觸發 enterRedirectToLogin
    onEnter: enterRedirectToLogin, 
    // 使用 onChange 當 routes location 變動導入底下 childRoutes path 時觸發 changeRedirectToLogin
    onChange: changeRedirectToLogin,
    childRoutes: [
      {
        path: '/',
        getComponent(location, cb) {
          require.ensure([], (require) => {
            cb(null, require('./Home/components/main').default)
          }, 'Home');
        }
      },
      {
        path: '/UserManage',
        getComponent(location, cb) {
          require.ensure([], (require) => {
            cb(null, require('./UserManage/components/main').default)
          },'UserManage');
        }
      }
    ]
  },
  {
    // 使用 onEnter 直接導入底下 childRoutes path 時觸發 redirectToMain
    onEnter: redirectToMain,
    childRoutes: [
      {
        path: '/Login',
        getComponent(location, cb) {
          require.ensure([], (require) => {
            cb(null, require('./Login/components/main').default)
          }, 'Login');
        }
      }
    ]
  }
  ]
};

主頁 index.js

...
..
.
import PageRoutes from './pageRoutes';
...
..
.
render(
  <Provider store={store}>
    <Router history={history} routes={PageRoutes} />
  </Provider>,
  reactElement
);

參考連結
https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback
https://github.com/reactjs/react-router/blob/master/docs/API.md#onchangeprevstate-nextstate-replace-callback

 
over 3 years ago

webpack.config.js 配置

module.exports = {
entry: {
    // 為主要起始頁面 js
    main: [
      './index.js'
    ],
    // 將常用的 Lib 配置到  vendor.bundle.js 檔案
    vendor: [
      'react', 'react-dom'
    ]
  },
  output: {
    path: path.resolve(__dirname, "build"),
    // 動態建立的主要 js 如 main.bundle.js
    filename: "[name].bundle.js",
    // 動態建立的 chunk js 如 Login.chunk.js
    chunkFilename: "[name].chunk.js"
  },
  ....
  ..
  .
  plugins:[
    // 使用 CommonsChunkPlugin 將共用的模組整到 vendor.bundle.js 內
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: 'vendor.bundle.js'
    }),
    // 壓縮醜化 js 檔案
    new webpack.optimize.UglifyJsPlugin({
          compressor: {
            warnings: false,
          },
    }),    
    // 使用 OccurenceOrderPlugin 減少文件大小
    new webpack.optimize.OccurenceOrderPlugin(),
    ...
    ..
    .
  ]
};

建立 pageRoutes.js

export default {
  childRoutes: [
    {
      path: '/',
      getComponent(location, cb) {
        require.ensure([], (require) => {
          cb(null, require('Home/components/main').default)
        }, 'Home'); // 產生 Home.chunk.js
      }
    },
    {
      path: '/Login',
      getComponent(location, cb) {
        require.ensure([], (require) => {
          cb(null, require('./Login/components/main').default)
        }, 'Login'); // 產生 Login.chunk.js
      }
    },
    {
      path: '/UserManage',
      getComponent(location, cb) {
        require.ensure([], (require) => {
          cb(null, require('./UserManage/components/main').default)
        },'UserManage'); // 產生 UserManage.chunk.js
      }
    }
  ]
};

搭配 react-router 的 getComponent
使用 require.ensure 及 require 產生 chunk 檔案
載入路徑必須為 extends Component 內容
如其他檔案需 import Home 相關內容, 需個別 import
Home/components/main 路徑為 components 路徑
內容如下

import React, {Component} from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';

class Main extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <ul>
          <li><Link to="/UserManage" >UserManage</Link></li>
      </ul>
    );
  }
}

export default Main;

主要 index.js 內容

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import store from 'store'
import { Router, Route, hashHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import createBrowserHistory from 'history/lib/createBrowserHistory'
import PageRoutes from 'pageRoutes';

const history = syncHistoryWithStore(hashHistory, store)
let reactElement = document.getElementById('react')
render(
  <Provider store={store}>
    <Router history={history} routes={PageRoutes} />
  </Provider>,
  reactElement
);

主要搭配 react-redux 及 react-router 和 react-router-redux

編譯結果


最後於 index.html 引用 vendor.bundle.js 及 main.bundle.js 即可

 
over 3 years ago

修正前

...
..
.
import * as actions from '../actions';
import { NAME } from '../constants';
import makeGetAllState from '../selectors';
import ValidateForm from '../../HOC/ValidateForm';
...
..
.

多了一些 ../ 及 ../../ 的相對路徑設定

追加 Webpack 的 modulesDirectories 設定來減少這類的相對路徑

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "http://localhost:8081/",
    filename: "bundle.js"
  },
  module: {
    ...
    ..
    .
  },
  resolve: {
    extensions: ['', '.js', '.jsx'],
    modulesDirectories: ['.', 'node_modules'],
  },
  ...
  ..
  .
};

修正後

...
..
.
import * as actions from 'actions';
import { NAME } from 'constants';
import makeGetAllState from 'selectors';
import ValidateForm from 'HOC/ValidateForm';
...
..
.

變得清爽多了 XD

參考網址
http://davidboyne.co.uk/2016/04/29/react-webpack-gem.html

 
over 3 years ago

這邊是搭配 Redux 及 react-bootstrap 做整合驗證
可修改HOC state 狀態來做變化
此範例產生的 state 範本
{ac:'{success | error}', acErroe:'{取至 error }'...}

import React from 'react';
import { findDOMNode } from 'react-dom';
import Immutable from 'immutable';

const ValidateForm = (Component, extendValidateMethods, validateItemIds) => class extends React.Component {
  constructor(props) {
    super(props);
    // 執行驗證函數
    this._checkForm = this._checkForm.bind(this);
    // 預設的驗證函數 固定參數 傳入主Component 及 要驗證的 子Component
    let _defaultValidateMethods = {
      empty: function (mainComponent, checkComponent) {
            let dom = findDOMNode(checkComponent);
            let val = dom.value;
            if(val == "") {
                return false;
            }
            var regu = "^[ ]+$";
            var re = new RegExp(regu);

            return !re.test(val);
        }
    }
    // 加入擴充函數 extendValidateMethods 可預設傳入 {}
    this._validateMethodSettings =  Object.assign(_defaultValidateMethods, extendValidateMethods);

    // 運用 Immutable 配合 validateItemIds 建立 HOC state
    let valstateImmutable = Immutable.Map({});
    for (let itemId of validateItemIds) {
       valstateImmutable = valstateImmutable.set(itemId, 'success');
       valstateImmutable = valstateImmutable.set(itemId + 'Error','');
    }
    this.state = {valstates: valstateImmutable};
  }
  _checkForm(e) {
    let component = this.refs.checkComponent;
    let state = this.state.valstates;
    let check = false;
    for (let itemId of validateItemIds) {
      // 取出要執行驗證的 checkMethod
      let runValidateMethod = this._validateMethodSettings[component.refs[itemId].props.checkMethod];
      // 執行 checkMethod 傳入主Component 及 要驗證的 子Component
      let result = runValidateMethod(component, component.refs[itemId]);
      let error = component.refs[itemId].props.error;
      // 設定錯誤或成功狀態
      if(!result) {
        state = state.set(itemId, 'error');
        state = state.set(itemId + 'Error',error);
        this.setState({valstates : state});
        check = true;
      } else {
        state = state.set(itemId, 'success')
        this.setState({valstates : state.set(itemId, 'success')});
      }
    }

    return check;
  }
  render() {
    return <Component ref="checkComponent" {...this.props} {...this.state} checkForm={this._checkForm} />
  }
}

export default ValidateForm;

使用範例

import { findDOMNode } from 'react-dom';
...
..
.

class Main extends Component {
  constructor(props) {
    super(props);
    this._onLogin = this._onLogin.bind(this);
  }
  _onLogin(e) {
    // 執行驗證 取得結果
    let check = this.props.checkForm();

    if (check) {
      return;
    }
    let acDom = findDOMNode(this.refs.ac);
    let pwDom = findDOMNode(this.refs.pw);
    this.props.actions.login(acDom.value, pwDom.value);
    this.props.actions.loginCheck();
  }
  render() {
    ....
    ...
    ..
    .
    // 提供 ref={驗證Id} checkMethod={驗證函數名稱} error={錯誤訊息}
    // this.props.valstates.get('ac') 取得驗證狀態 
    // this.props.valstates.get('acError') 取得錯誤訊息 取至 error="請輸入姓名"
    return (
      <form>
        <FormGroup controlId="formValidationError1" validationState={this.props.valstates.get('ac')}>
           <ControlLabel>帳號</ControlLabel>
           <FormControl type="text"  placeholder={this.props.valstates.get('acError')} ref="ac" checkMethod="empty"  error="請輸入姓名" />
       </FormGroup>
       <FormGroup controlId="formValidationError2" validationState={this.props.valstates.get('pw')}>
          <ControlLabel>密碼</ControlLabel>
          <FormControl type="password" placeholder={this.props.valstates.get('pwError')} ref="pw" checkMethod="compare" CompareId="ac" error="請輸入密碼"  />
      </FormGroup>
        <Button bsStyle="primary" onClick={this._onLogin}>登入</Button>
        {(isLoading) ? <div>load....</div> : <div>{msg}</div>}
      </form>
    );
  }
}

....
...
..
.
// 建立擴充驗證
const extendValidateMethods = {
  compare : (mainComponent, checkComponent) =>{
    let checkDom = findDOMNode(checkComponent);
    let compareDom = findDOMNode(mainComponent.refs[checkComponent.props.CompareId]);
    alert(`${checkDom.value} : ${compareDom.value}`);
    return false;
  }
};
// 建立檢查欄位Id (ref="ac")
const validateItemIds = ['ac', 'pw'];

// 搭配 redux 傳入 HOC ValidateForm, 如無 extendValidateMethods時可傳入 {}
export default connect(makeMapStateToProps, mapDispatchToProps)(ValidateForm(Main, extendValidateMethods, validateItemIds));

執行結果
預設 empty method

擴充 compare method

 
over 3 years ago

new Array()

範例

class Table extends PureComponent {
  render() {
return (
  <div>
    {this.props.items.map(i =>
      <Cell data={i} options={this.props.options || []} />
     )}
   </div>
 );
  }
}

每當執行 this.props.options || []
[] 如同 new Array() 會產生新的 array instance
導致淺層比對會一直出現 false 而讓每個 Cell components 每次都 re-render

修正

const default = [];
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || default} />
         )}
       </div>
     );
  }
}

建立一個預設的 [] 來避免重複性的 re-render

new Function

範例

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)} />;
  }
}
or
class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update.bind(this)} />;
 }
}

如同 [] 上面兩種配置 function 的方式也會產生 new Function instance

修正

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }
  update(e) {
    this.props.update(e.target.value);
  }
  render() {
    return <MyInput onChange={this.update} />;
  }
}

於 constructor 宣告配置 function 避開每次執行所產生的 new Function instance

使用 Reselect 配置 Redux connect(mapState)

範例

let App = ({otherData, resolution}) => (
  <div>
    <DataContainer data={otherData} />
    <ResolutionContainer resolution={resolution} />
  </div>
);
const doubleRes = (size) => ({
  width: size.width*2,
  height: size.height*2
});
App = connect(state => {
  return {
    otherData: state.otherData,
    resolution: doubleRes(state.resolution)
  }
})(App);

只要 otherData 一改變 DataContainer 和 ResolutionContainer component 就會跟著 render
即使 state.resolution 未做變動
那是因為 doubleRes function 永遠會return 一個 new resolution object

改用 reselect 優化 doubleRes function

import {createSelector} from “reselect”;
const doubleRes = createSelector(
  r => r.width,
  r => r.height,
  (width, height) => ({
    width: width*2,
    height: heiht*2
  })
);

Reselect 會記憶和 return 最後一次的資料
直到新的參數被更新

參考網址
https://medium.com/esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f#.rbs6epi7k@

 
over 3 years ago

Middleware 官方翻譯文件, 裡面有詳細的 Middleware 模式觀念
https://camsong.github.io/redux-in-chinese/docs/advanced/Middleware.html
其實就是實作 Currying 模式來暫存參數值
http://blog.rx836.tw/blog/javascript-currying/

利用 ES6 箭頭函數實現並觀察 Currying 模式的運作

// 初始化 Currying 函數
const testCurry = a => b => c => {
  return a + b + c;
}

// 檢視初始化的函數物件
console.log(testCurry);
/* log
function testCurry(a) {
      return function (b) {
        return function (c) {
          return a + b + c;
        };
      };
}
*/

// 傳遞第一個參數 a, 保存 a 後並回傳帶參數 b 的函數物件
const testOne = testCurry(1);
// 檢視回傳的函數物件
console.log(testOne);
/* log
function (b) {
     return function (c) {
          return a + b + c;
     };
}
*/
// 傳遞第二個參數 b, 保存 b 並回傳帶參數 c 的函數物件
const testTwo = testOne(2);
// 檢視回傳的函數物件
console.log(testTwo);
/* log
function (c) {
     return a + b + c;
}
*/
// 傳遞第二個參數 c 執行帶參數 c 的函數物件 return a + b + c;
console.log(testTwo(3));
// log 6

// 各種逐步呼叫方式
const testThree = testCurry(1);
console.log(testThree(2)(3));
// log 6
const testFour = testCurry(1)(2);
console.log(testFour(3));
// log 6
console.log(testCurry(1)(2)(3));
// log 6

而 Redux applyMiddleware 就是實踐逐步呼叫方式來執行相關的 Currying 模式物件

建立自訂 Redux Middleware

//官方提供的 Redux Middleware 範本 => 紀錄被觸發的action 及產生新的state
const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

// 使用 redux applyMiddleware 加入自訂 Middleware
let store =  applyMiddleware(thunk, logger)(createStore)(
  combineReducers({
    ...
    ..
  })
);

執行結果

redux-thunk 也是遵循相同的規範所建立的 Redux Middleware

export default function thunkMiddleware({ dispatch, getState }) {
  return next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  };
}
 
over 3 years ago

reselect 官方範例

import { createSelector } from 'reselect'

// 建立取出某個 state 的函數
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent

// 利用 createSelector 傳入 shopItemsSelector 函數物件
// items => shopItemsSelector return 的 物件
const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

// 利用 createSelector 傳入 shopItemsSelector, taxPercentSelector 函數物件
// subtotal => shopItemsSelector return 的 物件
// taxPercent => taxPercentSelector return 的 物件
const taxSelector = createSelector(
  shopItemsSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

// 可傳入上述利用 createSelector 所產生的 subtotalSelector, taxSelector 函數物件
// subtotal => subtotalSelector return 的 物件
// tax => taxSelector return 的 物件
export const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

// 測試用的 state
let exampleState = {
  taxPercent: 8,
  items: [
    { name: 'apple', value: 1.20 },
    { name: 'orange', value: 0.95 },
  ]
}

// 執行上面所寫好 Selector 函數
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState))      // 0.172
console.log(totalSelector(exampleState))    // { total: 2.322 }

React Redux 實作

建立 selectors.js
參考 https://github.com/reactjs/reselect#sharing-selectors-with-props-across-multiple-components

import { createSelector } from 'reselect';
// 設定某模組所要用的 state 名稱
import { NAME } from './constants';

// 取出 redux 配置 store 的 state 
const getAllState = (state, props) => state[NAME];

// 利用 return function 消除 cached 
const makeGetAllState = () => {
  return createSelector(
    getAllState,
    allState => {
      return {
        key: allState.get("key"),
        isLoading: allState.get("isLoading"),
        userlist: allState.get("userlist"),
        totalSize : allState.get("totalSize"),
        currentPage: allState.get("currentPage"),
        listSize: allState.get("listSize"),
      }
    }
  )
}

export default makeGetAllState;

於要執行 Redux 的 connect 內配置所建立好的 selectors
參考範例

import makeGetAllState from '../selectors';
...
..
const makeMapStateToProps= () => {
  const mapStateToProps = (state, props) => {
    return {
      // 使用建立好的 selectors 函數運算取出所要的 store state
      state: makeGetAllState()(state, props) 
    }
  }
  return mapStateToProps;
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch)
  }
}

export default connect(makeMapStateToProps, mapDispatchToProps)(Main)

搭配使用 redux-thunk 執行 Redux Async Actions

import makeGetAllState from '../selectors';
...
..
export function loadUserList() {
  return function(dispatch, getState) {
  const state = getState();
  const {key, currentPage, listSize} =  makeGetAllState()(state, null);
  ...
  ..
  .
}

使用 reselect 建立 selectors 抽出 Redux store 指定的 state
來更進一步於外部集中管理 state 內的資料
並減少 Component 內對 state 的邏輯運算

來源
https://github.com/reactjs/reselect