翻譯|如何使用React,Redux和Immutable.js構建Todo App

本文是翻譯版本,原文請見
By Dan Prince May 03, 2016

React使用組件和單向數據流方式描述用戶界面,但是React對state的處理非常的簡單.這一點讓我們知道,React僅僅只當于傳統的Model-View-Controller構架的View層.

僅僅使用React也可以構建大型的app,但是很快我們會發現,要保持代碼的簡潔,我們需要在其他地方管理state(把state的管理獨立出來).

沒有官方管理應用state的工具,但是有幾個庫工作的的不錯.今天我們添加兩個庫和React一起來構建一個簡單的app.

Redux

Redux是一個小型的js庫,作為app的state容器.糅合了Fluc和Elm的概念.我們可以使用Redux管理任何app的state,只要我們緊扣下面的指導:

  1. 我們的state保持在一個單一的store中
  2. state的改變只會來自于actions

Redux的核心 store是一個函數,它接收當前的application的state和一個action,合并創建一個新的application state,這個函數叫做Reducer.

我們的React組件負責發送actions到我們的store,反過來,如果組件需要渲染的時候,store會通知他.

ImmutableJS

因為Redux不允許我們mutate程序的state,如果借助immutable數據結構模型化應用程序的state將會非常的有用.
Immutable.js使用突變界面(mutative interfaces)提供一些immutable數據結構,這些界面實施時非常的高效,靈感來自于Clojure和Scala.

Demo

我們將會使用React,Redux和ImmutableJS去構建一個簡單的todo list,允許我們添加todos,在完成和未完成之間切換.

//html
 <div id="app"></div>

//css
 html, body, input, button {
  font-family: Sawasdee;
  font-size: 20px;
}

.todo {
}

.todo__list {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

.todo__item {
  padding: .5em .25em;
  border-bottom: solid 1px #eee;
}

.todo__item:hover {
  background: #f7f7f7;
  cursor: pointer;
}

.todo__entry {
  border: solid 1px #ccc;
  padding: .25em .5em;
  border-radius: .2em;
  background: #f3f3f3;
  width: 100%;
  box-sizing: border-box;
}

.todo__button {
  border: 0;
  border-radius: .2em;
  background: #71B7FF;  
  color: #fff;
  padding: .25em .5em;
  margin: .5em 0;
  margin-right: .25em;
  cursor: pointer;
}

.todo__button:hover {
  background: #B2D8FF;
}

//js
const { Map, List } = Immutable;
const { createStore } = Redux;
const { Provider, connect } = reactRedux;

const components = {
  Todo({ todo }) {
    if(todo.isDone) {
      return <strike>{todo.text}</strike>;  
    } else {
      return <span>{todo.text}</span>;
    }
  },
  TodoList({ todos, toggleTodo, addTodo }) {
    const onSubmit = (e) => {
      const text = e.target.value;
      if(e.which === 13 && text.length > 0) {
        addTodo(text);
        e.target.value = '';
      }
    };
    
    const toggleClick = (id) => () => toggleTodo(id);
    
    const { Todo } = components;
    
    return (
      <div className='todo'>
        <input type='text'
               className='todo__entry'
               placeholder='Add todo'
               onKeyDown={onSubmit} />
        <ul className='todo__list'>
          {todos.map(t => (
            <li
              key={t.get('id')}
              className='todo__item'
              onClick={toggleClick(t.get('id'))}>
              <Todo todo={t.toJS()} />
            </li>
          ))} 
        </ul>
      </div>
    );
  }
};

const actions = {
  addTodo(text) {
    return {
      type: 'ADD_TODO',
      payload: {
        id: Math.random().toString(34).slice(2),
        isDone: false,
        text
      }
    };
  },
  toggleTodo(id) {
    return {
      type: 'TOGGLE_TODO',
      payload: id
    }
  }
};

const init = List();

const reducer = function(state=init, action) {
  switch(action.type) {
    case 'ADD_TODO':
      return state.push(
        Map(action.payload)
      );
    case 'TOGGLE_TODO':
      return state.map(t => {
        if(t.get('id') == action.payload) {
          return t.update('isDone', isDone => !isDone);
        } else {
          return t;
        }
      });
    default:
      return state;
  }
};

const containers = {
  TodoList: connect(
    function mapStateToProps(state) {
      return {
        todos: state
      };
    },
    function mapDispatchToProps(dispatch) {
      return {
        toggleTodo: (id) => dispatch(actions.toggleTodo(id)),
        addTodo: (text) => dispatch(actions.addTodo(text))
      };
    }
  )(components.TodoList)
};

const { TodoList } = containers;
const store = createStore(reducer);

ReactDOM.render(
  <Provider store={store}>
    <TodoList />
  </Provider>,
  document.getElementById('app')
);

代碼在 Github

可能提示build失敗,npm install babel-core試試

setup

從創建項目??開始,建立一個package.json文件.然后安裝需要的依賴包.

 npm install --save react react-dom redux react-redux immutable
npm install --save-dev webpack babel-loader babel-preset-es2015 babel-preset-react

使用JSX和ES2015,用Babel編譯代碼,使用Webpack來完成這個模塊綁定過程.

webpack.config.js文件中創建Webpack配置文件.

 module.exports = {
  entry: './src/app.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: { presets: [ 'es2015', 'react' ] }
      }
    ]
  }
};

最后擴展一下package.json,添加一個npm script使用source maps編譯我們的代碼.

 "scripts": {
  "build": "webpack --debug"
}

每次編譯代碼的時候,運行npm run build.

React&Components

在實施項目之前,先創建一些傻瓜數據有很大的用處,但我們構思需要渲染的組件的時候,有一點點初步的感覺.

 const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

我們需要兩個React組件<Todo/><TodoList>

 // src/components.js

import React from 'react';

export function Todo(props) {
  const { todo } = props;
  if(todo.isDone) {
    return <strike>{todo.text}</strike>;
  } else {
    return <span>{todo.text}</span>;
  }
}

export function TodoList(props) {
  const { todos } = props;
  return (
    <div className='todo'>
      <input type='text' placeholder='Add todo' />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.id} className='todo__item'>
            <Todo todo={t} />
          </li>
        ))}
      </ul>
    </div>
  );
}

到了這一步,可以創建index.html文件來測試這些組價,添加下面的標記

 <!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <title>Immutable Todo</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

還有一個項目的入口文件src/app.js.

// src/app.js

import React from 'react';
import { render } from 'react-dom';
import { TodoList } from './components';

const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

render(
  <TodoList todos={dummyTodos} />,
  document.getElementById('app')
);

使用npm run build編譯文件,然后在瀏覽器中打開index.html文件,確保運行.

Redux&ImmutableJS

現在我們有了很好的UI,可以開始考慮組件最后的state.開始創建的傻瓜數據是一個很好的開端,我們可以很容易轉化為ImmutableJS集合.

 import { List, Map } from 'immutable';

const dummyTodos = List([
  Map({ id: 0, isDone: true,  text: 'make components' }),
  Map({ id: 1, isDone: false, text: 'design actions' }),
  Map({ id: 2, isDone: false, text: 'implement reducer' }),
  Map({ id: 3, isDone: false, text: 'connect components' })
]);

ImmutableJS map和Javascript的對象工作方式不同,所以我們要對組件做一點輕微的改變.property接入的地方(例如:todo.id)需要使用一個方法調用來代替(例如:todo.get(‘id’)).

設計Actions

現在我們獲得了數據的特征,可以考慮一下actions的更新.這個實例中,我們僅僅需要兩個acions,一個是添加新的todo,另一個轉換todo的狀態.

讓我們定義幾個函數創建這些actions

 // src/actions.js

// succinct hack for generating passable unique ids
const uid = () => Math.random().toString(34).slice(2);

export function addTodo(text) {
 return {
   type: 'ADD_TODO',
   payload: {
     id: uid(),
     isDone: false,
     text: text
   }
 };
}

export function toggleTodo(id) {
 return {
   type: 'TOGGLE_TODO',
   payload: id
 }
}

每一個action僅僅是一個有type和payload的屬性對象.在我們觸發action后,type屬性幫助我們用payload來作什么.

設計一個Reducer

現在我們知道了state的特性和更新state的action,我們可以創建reducer了.僅僅提醒一下,reducer是一個接收state和action的函數,然后用來計算更新state.

這里是我們reducer的初始結構.

 // src/reducer.js

import { List, Map } from 'immutable';

const init = List([]);

export default function(todos=init, action) {
  switch(action.type) {
    case 'ADD_TODO':
      // ...
    case 'TOGGLE_TODO':
      // ...
    default:
      return todos;
  }
}

操作ADD_TODOaction非常簡單,可是使用.push()方法,返回一個新的列表,添加todo到末尾.

 case 'ADD_TODO':
  return todos.push(Map(action.payload));

記住要push到列表之前,要把todo對象轉變為immutable map.

我們需要處理的稍微復雜的action是TOOGLE_TODO.

 case 'TOGGLE_TODO':
  return todos.map(t => {
    if(t.get('id') === action.payload) {
      return t.update('isDone', isDone => !isDone);
    } else {
      return t;
    }
  });

我們使用.map()遍歷列表,找到與acitonid匹配的todo項目.之后我們調用.update()方法,接收一個鍵和函數,然后返回一個map的新拷貝到updata函數,新拷貝中新值替換了初始值.

字面量版本

  const todo = Map({ id: 0, text: 'foo', isDone: false });
todo.update('isDone', isDone => !isDone);
// => { id: 0, text: 'foo', isDone: true }

把所有的東西都連系到一起

actions和reducer準備好了,可以創建一個store,連接到我們的React組件中.

 // src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { TodoList } from './components';
import reducer from './reducer';

const store = createStore(reducer);

render(
  <TodoList todos={store.getState()} />,
  document.getElementById('app')
);

為了保持組件和store的獨立,我們使用react-redux幫助簡化這個過程.它允許我們創建獨立于store的容器,包裝所有的組件,我們不需要改變先前的設計.

我們需要一個容器包裝<TodoList/>組件,看看下面的內容

 // src/containers.js

import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo } from './actions';

export const TodoList = connect(
  function mapStateToProps(state) {
    // ...
  },
  function mapDispatchToProps(dispatch) {
    // ...
  }
)(components.TodoList);

我們使用connect函數創建容器.當我們調用connect()函數,傳遞兩個函數,mapStateToProps()mapDispatchToProps().

mapStateToProps()函數接收當前store的state作為參數,期待返回一個我們包裝組件需要的對象映射.

 function mapStateToProps(state) {
  return { todos: state };
}

下面代碼是一個包裝組件根據映射map可視化的結果.

 <TodoList todos={state} />

我們也需要提供mapDispatchProps函數,傳遞store的dispatch方法,所以我們可以使用action creatros來dispatch actions.

 function mapDispatchToProps(dispatch) {
  return {
    addTodo: text => dispatch(addTodo(text)),
    toggleTodo: id => dispatch(toggleTodo(id))
  };
}

再一次實例化組件

 <TodoList todos={state}
          addTodo={text => dispatch(addTodo(text))}
          toggleTodo={id => dispatch(toggleTodo(id))} />

現在我們已經把action creators映射到組件,可以從事件監聽中調用.

 export function TodoList(props) {
  const { todos, toggleTodo, addTodo } = props;

  const onSubmit = (event) => {
    const input = event.target;
    const text = input.value;
    const isEnterKey = (event.which == 13);
    const isLongEnough = text.length > 0;

    if(isEnterKey && isLongEnough) {
      input.value = '';
      addTodo(text);
    }
  };

  const toggleClick = id => event => toggleTodo(id);

  return (
    <div className='todo'>
      <input type='text'
             className='todo__entry'
             placeholder='Add todo'
             onKeyDown={onSubmit} />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.get('id')}
              className='todo__item'
              onClick={toggleClick(t.get('id'))}>
            <Todo todo={t.toJS()} />
          </li>
        ))}
      </ul>
    </div>
  );
}


container容器自動訂閱store的變化,只要的映射的props變化的時候,容器包裝的組件就會重新渲染.

最后,需要使容器組件獨立于store,使用<Provider/>組件.

 // src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoList } from './containers';
//                          ^^^^^^^^^^

const store = createStore(reducer);

render(
  <Provider store={store}>
    <TodoList />
  </Provider>,
  document.getElementById('app')
);

結論

不可否認,對于初學者來說,React和Redux的生態系統是相當復雜和令人迷惑的.
但是好消息是這些概念是可以可以轉移的.我們僅僅粗略的接觸了Redux的基礎構架,但是已經足夠我們學習Elm 構架,或者選取ClojureScript庫例如:Om,Re-frame.類似的,我們僅僅看到immutable數據結構的只言片語,但是已經足夠我們學習Clojure或者Haskell.

不管你是剛開始探索有關state的web編程開發者,還是使用javascript很長時間的開發者,基于action構架的辦成和immutable數據結構變得至觀重要的技能.所以現在是學習這些內容的時間了.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容