本文是翻譯版本,原文請見
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數據結構變得至觀重要的技能.所以現在是學習這些內容的時間了.