前言
這篇文章是我在學習redux的過程中,在編碼的每一個階段編寫的學習筆記。項目地址:todo App。純react是在normal分支,結合redux的是在redux分支。react(15.6.1)和redux(3.7.2)都是當前(2017.8.4)的最新版本。所有的數據是存儲在localstorage下的,點擊查看demo。因為我的ui是移動端的,pc端沒有做處理,只能在移動端或是chrome下按CTRL+SHIFT+M進行查看。
演示

概念
action 來描述“發生了什么”
reducers 來根據 action 更新 state
Store 就是把它們聯系到一起的對象
action
Action 本質上是 JavaScript 普通對象。我們約定,action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作。
// action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const CHANGE_TAB = 'CHANGE_TAB';
const DELETE_TODO = 'DELETE_TODO';
// action constants
export const pageFilters = {
GO_LIST: 'GO_LIST',
GO_FORM: 'GO_FORM',
};
在 Redux 中的 action creators只是簡單的返回一個 action:
// action creators
// 添加一個todo任務
export function addTodo(newTodo) {
return { type: ADD_TODO, newTodo };
}
// 勾選完成/未完成的切換
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index };
}
// 列表頁與表單頁的切換
export function changeTab(filter) {
return { type: CHANGE_TAB, filter };
}
// 刪除一個todo
export function deleteTodo(index) {
return { type: DELETE_TODO, index };
}
Reducer
Action 只是描述了有事情發生了這一事實,并沒有指明應用如何更新 state。而這正是 reducer 要做的事情。
先設計一個state tree,用來存放所有的state。
const stateTree = {
pageFilter: 'GO_LIST', //當前頁
todos: [ //任務列表
{
text: '11',
place: '1',
time: '2:20',
content: 'dgusdguysdgisdhuis',
completed: false,
},
],
};
構造一個初始化state
const initialState = {
pageFilter: pageFilters.GO_LIST,
todos: [],
};
創建reducer。通過action的type改變state
// reducer一定為純函數,接受state和action,返回state。如果不存在state,則返回最初的state。
// (previousState, action) => newState
function todoApp(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
// 這里不能直接修改state,要使用 Object.assign() 新建了一個副本
return Object.assign({}, state, {
filter: action.filter,
});
default:
return state;
}
}
寫一下對其他action的處理,值得注意的是,都不要在原來的state上進行操作!!
function todoApp(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return Object.assign({}, state, {
filter: action.filter,
});
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
action.newTodo,
],
});
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (action.index === index) {
return Object.assign({}, todo, {
completed: !todo.completed,
});
}
return todo;
}),
});
case DELETE_TODO:
return Object.assign({}, state, {
todos: [].concat(state.todos).splice(action.index, 1),
});
default:
return state;
}
}
除了CHANGE_TAB
其他的action都是用來操作todos的,把它們拆分出來,使代碼結構看起來更加清晰。
// 操作state.todos
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state.todos,
action.newTodo,
];
case TOGGLE_TODO:
return state.todos.map((todo, index) => {
if (action.index === index) {
return Object.assign({}, todo, {
completed: !todo.completed,
});
}
return todo;
});
case DELETE_TODO:
return [].concat(state.todos).splice(action.index, 1);
default:
return state;
}
}
現在我們可以開發一個函數來做為主 reducer,它調用多個子 reducer 分別處理 state 中的一部分數據,然后再把這些數據合成一個大的單一對象。主 reducer 并不需要設置初始化時完整的 state。初始時,如果傳入 undefined, 子 reducer 將負責返回它們的默認值。
function todoApp(state = {}, action) {
return {
filter: filter(state.filter, action),
todos: todos(state.todos, action),
};
}
調用redux的工具類combineReducers
把子reducer整合在一起。
const todoApp = combineReducers({
filter,
todos,
});
Store
Store 有以下職責:
- 維持應用的 state;
- 提供 getState() 方法獲取 state;
- 提供 dispatch(action) 方法更新 state;
- 通過 subscribe(listener) 注冊監聽器;
- 通過 subscribe(listener) 返回的函數注銷監聽器。
創建一個store
import { createStore } from 'redux';
import todoApp from './reduces';
// Redux應用只有一個單一的store
// 根據已有的reducer創建store
// createStore第二個參數是可選的, 用于設置 state 初始狀態
const store = createStore(todoApp);
發起action
// 打印初始state狀態
console.log(store.getState());
// 每次 state 更新時,打印日志
// 注意 subscribe() 返回一個函數用來注銷監聽器
const unsubscribe = store.subscribe(() =>
console.log(store.getState()),
);
// 發起action
store.dispatch(changeTab(GO_FORM));
// 停止監聽 state 更新
unsubscribe();
可以從console中看到state的變化。
搭配 React
react與redux并沒有什么依賴關系,只不過react允許使用state函數的形式來描述界面。
先安裝react-redux
,使用<Provider>
把根組件包起來
import { Provider } from 'react-redux';
render(
<Provider store={store}>
<Root />
</Provider>
,
document.getElementById('root'),
);
Provider中的任何一個組件,想要使用store中的數據,就必須connect()
對其封裝。
connect
connect() 接收四個參數,它們分別是 mapStateToProps,mapDispatchToProps,mergeProps和options。
mapStateToProps 讀取state
在這里我們使用mapStateToProps
這個函數,將store中的數據作為props綁定到組件上。
// 構造一個函數返回需要的state
const mapStateToProps = (state) => {
const { filter, todos } = state;
return {
filter,
todos,
};
};
// 使用connect將state作為props傳入root中
export default connect(mapStateToProps)(Root);
這時,<Root/>
中的就有了如下的props
{
dispatch:function dispatch(action),
filter:"GO_LIST",
todos:Array(0)
}
分發action
定義mapDispatchToProps()
方法接受dispatch()
方法,并返回一個回調函數,用來分發action。
// 構造一個函數來分發action
const mapDispatchToProps = dispatch => ({
goPages: (pageName) => {
dispatch(changeTab(pageName));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Root);
這時再打印一下<Root/>
里的props
{
filter:"GO_LIST",
goPages:pageName => {…},
todos:Array(0)
}
在調用時,作為props傳遞給子組件,如:
<AddPage changeTab={() => goPages(pageFilters.GO_LIST)} data={this.state.data} />