7.3 Redux 实战入门
前言
上一节我们了解了 Redux 基本的概念和特性后,本章我们要实际动手用 Redux、React Redux 结合 ImmutableJS 开发一个简单的 Todo 应用。话不多说,那就让让我们开始吧!
以下这张图表示了整个 React Redux App 的资料流程图(使用者与 View 互动 => dispatch 出 Action => Reducers 依据 action tyoe 分配到对应处理方式,回传新的 state => 透过 React Redux 传送给 React,React 重新绘製 View):
动手创作 React Redux ImmutableJS TodoApp
在开始创作之前我们先完成一些开发的前置作业,先透过以下指令在根目录产生 npm 设定档 package.json
:
$ npm init
安装相关套件(包含开发环境使用的套件):
$ npm install --save react react-dom redux react-redux immutable redux-actions redux-immutable
$ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react html-webpack-plugin webpack webpack-dev-server
安装好后我们可以设计一下我们的资料夹结构,首先我们在根目录建立 src
,放置 script
的 source
。在 components
资料夹中我们会放置所有 components
(个别元件资料夹中会用 index.js
输出元件,让引入元件更简洁)、containers
(负责和 store 互动取得 state),另外还有 actions
、constants
、reducers
、store
,其余设定档则放置于根目录下。
大致上的资料夹结构会长这样:
接下来我们参考上一章设定一下开发文档(.babelrc
、.eslintrc
、webpack.config.js
)。这样我们就完成了开发环境的设定可以开始动手实作 React Redux
应用程式了!
首先我们先用 Component 之眼感受一下我们应用程式,将它切成一个个 Component
。在这边我们设计一个主要的 Main
包含两个子 Component:TodoHeader
、TodoList
。
首先设计 HTML Markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Redux Todo</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
在撰写 src/index.js
之前,我们先说明整合 react-redux
的用法。从以下这张图可以看到 react-redux
是 React 和 Redux 间的桥樑,使用 Provider
、connect
去连结 store
和 React View。
事实上,整合了 react-redux
后,我们的 React App 就可以解决传统跨 Component 之前传递 state 的问题和困难。只要透过 Provider
就可以让每个 React App 中的 Component
取用 store 中的 state,非常方便(接下来我们也会更详细说明 Container/Component、connect
的用法)。
以下是 src/index.js
完整程式码:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import Main from './components/Main';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<Main />
</Provider>,
document.getElementById('app')
);
其中 src/components/Main/Main.js
是 Stateless Component,负责所有 View 的进入点。
import React from 'react';
import ReactDOM from 'react-dom';
import TodoHeaderContainer from '../../containers/TodoHeaderContainer';
import TodoListContainer from '../../containers/TodoListContainer';
const Main = () => (
<div>
<TodoHeaderContainer />
<TodoListContainer />
</div>
);
export default Main;
接下来我们定义一下 Actions
的部份,由于是範例 App 所以相对简单,这边只定义一个 todoActions。在这边我们使用了 redux-actions,它可以方便我们使用 Flux Standard Action 格式的 action。以下是 src/actions/todoActions.js
完整程式码:
import { createAction } from 'redux-actions';
import {
CREATE_TODO,
DELETE_TODO,
CHANGE_TEXT,
} from '../constants/actionTypes';
export const createTodo = createAction('CREATE_TODO');
export const deleteTodo = createAction('DELETE_TODO');
export const changeText = createAction('CHANGE_TEXT');
我们在 src/actions/index.js
将所有 actions 输出
export * from './todoActions';
另外我们把 constants 放到 components
资料夹中方便管理,以下是 src/constants/actionTypes.js
程式码:
export const CREATE_TODO = 'CREATE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const CHANGE_TEXT = 'CHANGE_TEXT';
/*
或是可以考虑使用 keyMirror,方便产生与 key 相同的常数
import keyMirror from 'fbjs/lib/keyMirror';
export default keyMirror({
ADD_ITEM: null,
DELETE_ITEM: null,
DELETE_ALL: null,
FILTER_ITEM: null
});
*/
设定 Actions 后我们来讨论一下 Reducers 的部份。在讨论 Reducers 之前我们先来设定一下我们的前端的资料结构,在这边我们把所有资料结构(initialState)放到 src/constants/models.js
中。这边特别注意的是由于 Redux 中有一个重要特性是 State is read-only
,也就是说更新当 reducers 进到 action 只会回传新的 state 不会更改到原有的 state。因此我们会在整个 Redux App 中使用 ImmutableJS
让整个资料流维持在 Immutable
的状态,也可以提昇程式开发上的效能和避免不可预期的副作用。
以下是 src/constants/models.js
完整程式码,其设定了 TodoState 的资料结构并使用 fromJS()
转成 Immutable
:
import Immutable from 'immutable';
export const TodoState = Immutable.fromJS({
'todos': [],
'todo': {
id: '',
text: '',
updatedAt: '',
completed: false,
}
});
接下来我们要讨论的是 Reducers 的部份,在 todoReducers
中我们会根据接收到的 action 进行 mapping 到对应的处理函式并传入夹带的 payload
资料(这边我们使用 redux-actions 来进行 mapping,使用上比传统的 switch 更为简洁)。Reducers 接收到 action 的处理方式为 (initialState, action) => newState
,最终会回传一个新的 state,而非更改原来的 state,所以这边我们使用 ImmutableJS
。
import { handleActions } from 'redux-actions';
import { TodoState } from '../../constants/models';
import {
CREATE_TODO,
DELETE_TODO,
CHANGE_TEXT,
} from '../../constants/actionTypes';
const todoReducers = handleActions({
CREATE_TODO: (state) => {
let todos = state.get('todos').push(state.get('todo'));
return state.set('todos', todos)
},
DELETE_TODO: (state, { payload }) => (
state.set('todos', state.get('todos').splice(payload.index, 1))
),
CHANGE_TEXT: (state, { payload }) => (
state.merge({ 'todo': payload })
)
}, TodoState);
export default todoReducers;
import { handleActions } from 'redux-actions';
import UiState from '../../constants/models';
export default handleActions({
SHOW: (state, { payload }) => (
state.set('todos', payload.todo)
),
}, UiState);
虽然 Redux 本身仅会有一个 store,但 redux 本身有提供了 combineReducers
可以让我们切割我们 state 方便维护和管理。实上,state 的规划也是一们学问,通常需要不断地实作和工作团队讨论才能找到比较好的方式。不过这边要注意的是我们改使用了 redux-immutable
的 combineReducers
这样可以确保我们的 state 维持在 Immutable
的状态。
由于 Redux 官方也没有特别明确或严谨的规範。在一般情况我会将 reducers 分为 data
和单纯和 UI 有关的 ui
state。但由于这边是比较简单的例子,我们最终只使用到 src/reducers/data/todoReducers.js
。
import { combineReducers } from 'redux-immutable';
import ui from './ui/uiReducers';// import routes from './routes';
import todo from './data/todoReducers';// import routes from './routes';
const rootReducer = combineReducers({
todo,
});
export default rootReducer;
还记得我们上面说明 React Redux 之前的桥樑时有提到的 store 吗?现在我们要更仔细地去设计 store
,我们这边使用到了 redux 其中两个 API:applyMiddleware、createStore。分别可以产生 store 和挂载我们要使用的 middleware(这边我们只使用到 redux-logger 方便我们除错)。注意我们 initialState 也是维持在 Immutable
的状态。
import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import Immutable from 'immutable';
import rootReducer from '../reducers';
const initialState = Immutable.Map();
export default createStore(
rootReducer,
initialState,
applyMiddleware(createLogger({ stateTransformer: state => state.toJS() }))
);
透过 src/store/index.js
输出 configureStore:
export { default } from './configureStore';
讲解完架构层面的议题,终于我们来到了 View 的部份。加油,距离我们终点也不远了!
在开始讨论 Component
的部份之前我们先来研究一下
react-redux 所提供的 API connect
将 props 传给 Component,其用法如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
在我们的範例 App 中我们只会先用到前两个参数,第三个参数会在之后的例子里用到。第一个参数 mapStateToProps 是一个让开发者可以从 store 取出想要 state 并当做 props 往下传的功能,第二个参数则是将 dispatch 行为封装成函数顺着 props 可以方便往下传和呼叫。
以下是 src/components/TodoHeader/TodoHeader.js
的部份:
import React from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import TodoHeader from '../../components/TodoHeader';
// 将欲使用的 actions 引入
import {
changeText,
createTodo,
} from '../../actions';
const mapStateToProps = (state) => ({
// 从 store 取得 todo state
todo: state.getIn(['todo', 'todo'])
});
const mapDispatchToProps = (dispatch) => ({
// 当使用者在 input 输入资料值即会触发这个函数,发出 changeText action 并附上使用者输入内容 event.target.value
onChangeText: (event) => (
dispatch(changeText({ text: event.target.value }))
),
// 当使用者按下送出时,发出 createTodo action 并清空 input
onCreateTodo: () => {
dispatch(createTodo());
dispatch(changeText({ text: '' }));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodoHeader);
// 开始建设 Component 并使用 connect 进来的 props 并绑定事件(onChange、onClick)。注意我们的 state 因为是使用 `ImmutableJS` 所以要用 `get()` 取值
const TodoHeader = ({
onChangeText,
onCreateTodo,
todo,
}) => (
<div>
<h1>TodoHeader</h1>
<input type="text" value={todo.get('text')} onChange={onChangeText} />
<button onClick={onCreateTodo}>送出</button>
</div>
);
export default TodoHeader;
以下是 src/components/TodoList/TodoList.js
的部份:
import React from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import TodoList from '../../components/TodoList';
import {
deleteTodo,
} from '../../actions';
const mapStateToProps = (state) => ({
todos: state.getIn(['todo', 'todos'])
});
// 由 Component 传进欲删除元素的 index
const mapDispatchToProps = (dispatch) => ({
onDeleteTodo: (index) => () => (
dispatch(deleteTodo({ index }))
)
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodoList);
// Component 部分值的注意的是 todos state 是透过 map function 去迭代出元素,由于要让 React JSX 可以渲染并保持传入触发 event state 的 immutable,所以需使用 toJS() 转换 component of array。
const TodoList = ({
todos,
onDeleteTodo,
}) => (
<div>
<ul>
{
todos.map((todo, index) => (
<li key={index}>
{todo.get('text')}
<button onClick={onDeleteTodo(index)}>X</button>
</li>
)).toJS()
}
</ul>
</div>
);
export default TodoList;
若是一切顺利的话就可以在浏览器上看到自己努力的成果啰!(因为我们有使用 redux-logger
所以打开 console 会看到 action 和 state 的变化情形,但记得在 production
环境要拿掉)
总结
以上就是 Redux 实战入门,对于第一次自己动手写 Redux 的朋友可能会需要多练习几次,多体会整个架构。在接下来的章节我们将优化我们的 React Redux TodoApp,让它可以有更清晰好维护的架构。