本文是翻译版本,原文请见
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,只要我们紧扣下面的指导:
- 我们的state保持在一个单一的store中
- 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_TODO
action非常简单,可是使用.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数据结构变得至观重要的技能.所以现在是学习这些内容的时间了.