翻译|如何使用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数据结构变得至观重要的技能.所以现在是学习这些内容的时间了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容