Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。 (如果你需要一個 WordPress 框架,請查看 Redux Framework。)
Redux 除了和 React 一起用外,還支持其它界面庫。 它體小精悍(只有 2kB,包括依賴)。
在React項目中使用redux
安裝:
npm install --save redux
yarn add redux
基本概念
Redux的設計理念:Web 應用是一個狀態機,視圖與狀態是一一對應的。所有的狀態,保存在一個對象里面。只能按照Redux提供的約定的方式對狀態進行編輯。
Store
Store 就是保存數據的地方,你可以把它看成一個容器。整個應用只能有一個 Store。另外Store是整個Redux的統一操作的入口。
Redux 提供createStore這個函數,用來生成 Store。
import { createStore } from 'redux'; const store = createStore(fn);
上面代碼中,createStore函數接受另一個函數作為參數,返回新生成的 Store 對象。
State
Store對象包含所有數據。如果想得到某個時點的數據,就要對 Store 生成快照。這種時點的數據集合,就叫做 State。
當前時刻的 State,可以通過store.getState()拿到。
import { createStore } from 'redux'; const store = createStore(fn); const state = store.getState();
Redux 規定, 一個 State 對應一個 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么樣,反之亦然。
Action
State 的變化,會導致 View 的變化。但是,用戶接觸不到 State,只能接觸到 View。所以,State 的變化必須是 View 導致的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。
Action 是一個對象。其中的type屬性是必須的,表示 Action 的名稱。其他屬性可以自由設置,社區有一個規范可以參考。
const action = {
type: 'ADD_TODO', payload: 'Learn Redux' };
上面代碼中,Action 的名稱是ADD_TODO,它攜帶的信息是字符串Learn Redux。
可以這樣理解,Action 描述當前發生的事情。改變 State 的唯一辦法,就是使用 Action。它會運送數據到 Store。
Action Creator
View 要發送多少種消息,就會有多少種 Action。如果都手寫,會很麻煩。可以定義一個函數來生成 Action,這個函數就叫 Action Creator。
const ADD_TODO = '添加 TODO'; function addTodo(text) { return { type: ADD_TODO, text } } const action = addTodo('Learn Redux');
上面代碼中,addTodo函數就是一個 Action Creator。
Reducer
Store 收到 Action 以后,必須給出一個新的 State,這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。
Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State
const reducer = function (state, action) { // ... return new_state; };
Dispatcher
store.dispatch()
是 View 發出 Action 的唯一方法。
import { createStore } from 'redux'; const store = createStore(fn); store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' });
上面代碼中,store.dispatch接受一個 Action 對象作為參數,將它發送出去。
結合 Action Creator,這段代碼可以改寫如下。
store.dispatch(addTodo('Learn Redux'));
用一個圖來完整的展現他們之間的關系:

Subscribe
Store 允許使用store.subscribe方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。
import { createStore } from 'redux'; const store = createStore(reducer); store.subscribe(listener); 顯然,只要把 View 的更新函數(對於 React 項目,就是組件的render方法或setState方法)放入listen,就會實現 View 的自動渲染。 store.subscribe方法返回一個函數,調用這個函數就可以解除監聽。 let unsubscribe = store.subscribe(() => console.log(store.getState()) ); unsubscribe();
Store高級
構建Store
store是整個Redux的操作的入口。構建Store的時候還可以指定中間件和Reducer及默認的state。
- 構建store
import { createStore } from 'redux'; let store = createStore(reducer);
- 構建帶默認state的store
import { createStore } from 'redux'; let store = createStore(reducer, initialState);
- 構建帶中間件的store
applyMiddleware
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; // 日志中間件 const store = createStore( reducer, initial_state, applyMiddleware(logger) );
當然可以構建帶多個中間件的store
const store = createStore( reducer, applyMiddleware(thunk, promise, logger) );
Stroe的方法
store.getState()
獲取整個狀態數據對象。store.dispatch()
分發Actionstore.subscribe()
訂閱狀態數據的變化
import { createStore } from 'redux'; let { subscribe, dispatch, getState } = createStore(reducer);
綜合實例:計數器實例:
import React, { Component } from 'react' import {createStore, combineReducers} from 'redux'; const ActionTypes = { ADD_NUM: 'ADD_NUM', MINUSE_NUM: 'MINUSE_NUM' }; const ActionCreators = { AddNum(num) { return { type: ActionTypes.ADD_NUM, payload: num } }, MinusNum(num) { return { type: ActionTypes.MINUSE_NUM, payload: num } } } // 狀態樹中就只有一個值 Num的值。 const numReducer = (state=0, action) => { switch(action.type) { case ActionTypes.ADD_NUM : return state + action.payload; case ActionTypes.MINUSE_NUM : return state - action.payload default: return state; } }; const store = createStore(numReducer); class Count extends Component { constructor (props, context) { super(props, context) this.state ={ Num: 0 } } componentDidMount() { // 訂閱store的變化。 store.subscribe(() => { this.setState({ Num: store.getState() // 獲取最新的state的狀態 }) }); } render () { return ( <div> <p>{ store.getState() }</p> <p>{ this.state.Num }</p> <button onClick={ () => { store.dispatch(ActionCreators.AddNum(1)) }} > +1 </button> <button onClick={ () => { store.dispatch(ActionCreators.MinusNum(1)) }} > -1 </button> </div> ) } } export default Count
Reducer 的拆分
Reducer 函數負責生成 State。由於整個應用只有一個 State 對象,包含所有數據,對於大型應用來說,這個 State 必然十分龐大,導致 Reducer 函數也十分龐大。
redux提供了combineReducers
方法協助我們把狀態對應的Reducer進行拆分。單獨狀態對應的Reducer進行單獨編寫。combineReducers
可以將各個子 Reducer 函數合成一個大的 Reducer。
import { combineReducers } from 'redux'; const chatReducer = combineReducers({ chatLog, statusMessage, userName }) export default todoApp;
合並的Reducer中的key就是我們的狀態樹中的屬性名。
例如:
//首頁得文字 function titleReducer(state = 'aicoder全棧實習', action) { if (action.type === 'EDIT_APP_TITLE') { return action.payload; } else { return state; } } function LoginUserReducer(state = null, action) { if (action.type === 'LOGIN') { return action.payload; } else { return state; } } import { combineReducers } from 'redux'; const chatReducer = combineReducers({ Title: titleReducer, LoginUser: LoginUserReducer }) ....
redux-thunk 中間件
redux-thunk
中間件改造了redux的dispatch方法允許我們用store.dispatch(fn)
, fn
可以是一個函數。而且此函數可以接受兩個參數:dispatch
、getState
做為參數。
安裝
npm install redux-thunk
配置中間件
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; // Note: this API requires redux@>=3.1.0 const store = createStore( reducer, applyMiddleware(thunk) ); const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; function increment() { return { type: INCREMENT_COUNTER }; } function incrementAsync() { return dispatch => { setTimeout(() => { // Yay! Can invoke sync or async actions with `dispatch` dispatch(increment()); }, 1000); }; } store.dispatch(incrementAsync());
redux-thunk中間件的返回值的處理
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; // Note: this API requires redux@>=3.1.0 const store = createStore( reducer, applyMiddleware(thunk) ); // 異步刪除的方法 function asyncDelStu(stuId) { return (dispatch, getState) => { return axios .post('http://yapi.demo.qunar.com/mock/7378/api/delstu') .then(res => { // getState dispatch(delStu(stuId)); }) .catch((res) => { console.log(res); }); } } store .dispatch(asyncDelStu(33)) // 執行完成dispatch后,如果內部有返回值,此處還可以拿到返回值的結果。 .then(res => { console.log(res.data) }) .catch(e => {})
案例
import React, {Component} from 'react' import {createStore, applyMiddleware} from 'redux'; import thunk from 'redux-thunk' const ActionTypes = { ADD_NUM: 'ADD_NUM', MINUSE_NUM: 'MINUSE_NUM', INIT_NUM: 'INIT_NUM' }; const ActionCreators = { AddNum(num) { return {type: ActionTypes.ADD_NUM, payload: num} }, MinusNum(num) { return {type: ActionTypes.MINUSE_NUM, payload: num} }, AddNumAsync(num) { // ** 核心: 添加異步增加的方法 return (dispatch, getState) => { return new Promise((resolve, reject) => { setTimeout(() => { dispatch(ActionCreators.AddNum(num)); resolve(num); }, 1000); }); } } } const numReducer = (state = 0, action) => { switch (action.type) { case ActionTypes.ADD_NUM: return state + action.payload; case ActionTypes.MINUSE_NUM: return state - action.payload default: return state; } }; const store = createStore(numReducer,applyMiddleware(thunk)); class Count extends Component { constructor(props, context) { super(props, context) this.state = { Num: 0 } } componentDidMount() { store.subscribe(() => { this.setState({ Num: store.getState() }) }); } render() { return ( <div> <p>{store.getState()}</p> <p>{this.state.Num}</p> <button onClick={() => { store.dispatch(ActionCreators.AddNum(1)) }}> +1 </button> <button onClick={() => { store .dispatch(ActionCreators.AddNumAsync(2)) // dispatch一個函數 .then(res => console.log(res)) .catch(e => console.log(e)) }}> async+1 </button> <button onClick={() => { store.dispatch(ActionCreators.MinusNum(1)) }}> -1 </button> </div> ) } } export default Count
redux-promise中間件
redux-promise 中間件 既然 Action Creator 可以返回函數,當然也可以返回其他值。另一種異步操作的解決方案,就是讓 Action Creator 返回一個 Promise 對象。
這就需要使用redux-promise中間件。
import { createStore, applyMiddleware } from 'redux'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers'; const store = createStore( reducer, applyMiddleware(promiseMiddleware) );
這個中間件使得store.dispatch方法可以接受 Promise 對象作為參數。這時,Action Creator 有兩種寫法。寫法一,返回值是一個 Promise 對象。
const fetchPosts = (dispatch, postTitle) => new Promise(function (resolve, reject) { dispatch(requestPosts(postTitle)); return fetch(`/some/API/${postTitle}.json`) .then(response => { type: 'FETCH_POSTS', payload: response.json() }); });
redux-promise實例
import React, {Component} from 'react' import {createStore, combineReducers, applyMiddleware} from 'redux'; import thunk from 'redux-thunk' import redudxPromise from 'redux-promise'; import {composeWithDevTools} from 'redux-devtools-extension'; const ActionTypes = { ADD_NUM: 'ADD_NUM', MINUSE_NUM: 'MINUSE_NUM', INIT_NUM: 'INIT_NUM' }; const ActionCreators = { AddNum(num) { return {type: ActionTypes.ADD_NUM, payload: num} }, MinusNum(num) { return {type: ActionTypes.MINUSE_NUM, payload: num} }, AddNumAsync(num) { return (dispatch, getState) => { return new Promise((resolve, reject) => { setTimeout(() => { dispatch(ActionCreators.AddNum(num)); resolve(num); }, 1000); }); } }, MinusNumAsyncPromise(num) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(ActionCreators.MinusNum(num)); }, 1000); }) } } const numReducer = (state = 0, action) => { switch (action.type) { case ActionTypes.ADD_NUM: return state + action.payload; case ActionTypes.MINUSE_NUM: return state - action.payload default: return state; } }; // 此處由於是開發階段用了redex的開發工具所以不用考慮以下的變化。 const store = createStore(numReducer, composeWithDevTools(applyMiddleware(thunk, redudxPromise), // other store enhancers if any )); class Count extends Component { constructor(props, context) { super(props, context) this.state = { Num: 0 } } componentDidMount() { store.subscribe(() => { this.setState({ Num: store.getState() }) }); } render() { return ( <div> <p>{store.getState()}</p> <p>{this.state.Num}</p> <button onClick={() => { store.dispatch(ActionCreators.AddNum(1)) }}> +1 </button> <button onClick={() => { store .dispatch(ActionCreators.AddNumAsync(2)) .then(res => console.log(res)) .catch(e => console.log(e)) }}> async+1 </button> <button onClick={() => { store.dispatch(ActionCreators.MinusNum(1)) }}> -1 </button> <button onClick={() => { store.dispatch(ActionCreators.MinusNumAsyncPromise(1)) .then(res => console.log(res)) }}> async Promise -1 </button> </div> ) } } export default Count
React-Redux 的用法
這個庫是可以選用的。實際項目中,你應該權衡一下,是直接使用 Redux,還是使用 React-Redux。后者雖然提供了便利,但是需要掌握額外的 API,並且要遵守它的組件拆分規范。 React-Redux 將所有組件分成兩大類:UI 組件(presentational component)和容器組件(container component)。
安裝
npm install --save react-redux
UI 組件
UI 組件有以下幾個特征。
- 只負責 UI 的呈現,不帶有任何業務邏輯
- 沒有狀態(即不使用this.state這個變量)
- 所有數據都由參數(this.props)提供
- 不使用任何 Redux 的 API
- 下面就是一個 UI 組件的例子。
const Title = value => <h1>{value}</h1>;
因為不含有狀態,UI 組件又稱為"純組件",即它純函數一樣,純粹由參數決定它的值。
容器組件
- 負責管理數據和業務邏輯,不負責 UI 的呈現
- 帶有內部狀態
- 使用 Redux 的 API 總之,只要記住一句話就可以了:UI 組件負責 UI 的呈現,容器組件負責管理數據和邏輯。
React-Redux 規定,所有的 UI 組件都由用戶提供,容器組件則是由 React-Redux 自動生成。也就是說,用戶負責視覺層,狀態管理則是全部交給它。
connect() 連接容器組件和UI組件
React-Redux 提供connect方法,用於從 UI 組件生成容器組件。connect的意思,就是將這兩種組件連起來。
import { connect } from 'react-redux' const VisibleTodoList = connect()(TodoList);
上面代碼中,TodoList是 UI 組件,VisibleTodoList就是由 React-Redux 通過connect方法自動生成的容器組件。
但是,因為沒有定義業務邏輯,上面這個容器組件毫無意義,只是 UI 組件的一個單純的包裝層。為了定義業務邏輯,需要給出下面兩方面的信息。
-
(1)輸入邏輯:外部的數據(即state對象)如何轉換為 UI 組件的參數
-
(2)輸出邏輯:用戶發出的動作如何變為 Action 對象,從 UI 組件傳出去。
因此,connect方法的完整 API 如下。
import { connect } from 'react-redux' const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList)
上面代碼中,connect方法接受兩個參數:mapStateToProps和mapDispatchToProps。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state映射到 UI 組件的參數(props),后者負責輸出邏輯,即將用戶對 UI 組件的操作映射成 Action。
mapStateToProps()
mapStateToProps是一個函數。它的作用就是像它的名字那樣,建立一個從(外部的)state對象到(UI 組件的)props對象的映射關系。
作為函數,mapStateToProps執行后應該返回一個對象,里面的每一個鍵值對就是一個映射。請看下面的例子。
const mapStateToProps = (state) => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
上面代碼中,mapStateToProps是一個函數,它接受state作為參數,返回一個對象。這個對象有一個todos屬性,代表 UI 組件的同名參數,后面的getVisibleTodos也是一個函數,可以從state算出 todos 的值。
下面就是getVisibleTodos的一個例子,用來算出todos。
const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) default: throw new Error('Unknown filter: ' + filter) } }
mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,重新計算 UI 組件的參數,從而觸發 UI 組件的重新渲染。
mapStateToProps的第一個參數總是state對象,還可以使用第二個參數,代表容器組件的props對象。
// 容器組件的代碼 // <FilterLink filter="SHOW_ALL"> // All // </FilterLink> const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter } }
使用ownProps作為參數后,如果容器組件的參數發生變化,也會引發 UI 組件重新渲染。
connect方法可以省略mapStateToProps參數,那樣的話,UI 組件就不會訂閱Store,就是說 Store 的更新不會引起 UI 組件的更新。
mapDispatchToProps()
mapDispatchToProps是connect函數的第二個參數,用來建立 UI 組件的參數到store.dispatch方法的映射。也就是說,它定義了哪些用戶的操作應該當作 Action,傳給 Store。它可以是一個函數,也可以是一個對象。
如果mapDispatchToProps是一個函數,會得到dispatch和ownProps(容器組件的props對象)兩個參數。
const mapDispatchToProps = ( dispatch, ownProps ) => { return { onClick: () => { dispatch({ type: 'SET_VISIBILITY_FILTER', filter: ownProps.filter }); } }; }
從上面代碼可以看到,mapDispatchToProps作為函數,應該返回一個對象,該對象的每個鍵值對都是一個映射,定義了 UI 組件的參數怎樣發出 Action。
如果mapDispatchToProps是一個對象,它的每個鍵名也是對應 UI 組件的同名參數,鍵值應該是一個函數,會被當作 Action creator ,返回的 Action 會由 Redux 自動發出。舉例來說,上面的mapDispatchToProps寫成對象就是下面這樣。
const mapDispatchToProps = { onClick: (filter) => { type: 'SET_VISIBILITY_FILTER', filter: filter }; }
<Provider>
組件
connect方法生成容器組件以后,需要讓容器組件拿到state對象,才能生成 UI 組件的參數。
一種解決方法是將state對象作為參數,傳入容器組件。但是,這樣做比較麻煩,尤其是容器組件可能在很深的層級,一級級將state傳下去就很麻煩。
React-Redux 提供Provider組件,可以讓容器組件拿到state。
import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
上面代碼中,Provider在根組件外面包了一層,這樣一來,App的所有子組件就默認都可以拿到state了。
store放在了上下文對象context上面。然后,子組件就可以從context拿到store,代碼大致如下。
class VisibleTodoList extends Component { componentDidMount() { const { store } = this.context; this.unsubscribe = store.subscribe(() => this.forceUpdate() ); } render() { const props = this.props; const { store } = this.context; const state = store.getState(); // ... } } VisibleTodoList.contextTypes = { store: React.PropTypes.object }
React-Redux自動生成的容器組件的代碼,就類似上面這樣,從而拿到store。
React-Redux 完整demo
App.js代碼
import React, { Component } from 'react'; import RRDemo from './components/RRDemo'; class App extends Component { render() { return ( <div> <RRDemo></RRDemo> </div> ); } } export default App;
index.js代碼
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import {Provider} from 'react-redux'; import store from './store'; ReactDOM.render( <Provider store={store}> <App></App> </Provider> , document.getElementById('root'));
store.js代碼
import {createStore, applyMiddleware} from 'redux'; import thunk from 'redux-thunk' import redudxPromise from 'redux-promise'; import {composeWithDevTools} from 'redux-devtools-extension'; export const ActionTypes = { ADD_NUM: 'ADD_NUM', MINUSE_NUM: 'MINUSE_NUM', INIT_NUM: 'INIT_NUM' }; export const ActionCreators = { AddNum(num) { return {type: ActionTypes.ADD_NUM, payload: num} }, MinusNum(num) { return {type: ActionTypes.MINUSE_NUM, payload: num} }, AddNumAsync(num) { return (dispatch, getState) => { return new Promise((resolve, reject) => { setTimeout(() => { dispatch(ActionCreators.AddNum(num)); resolve(num); }, 1000); }); } }, MinusNumAsyncPromise(num) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(ActionCreators.MinusNum(num)); }, 1000); }) } } const numReducer = (state = 0, action) => { switch (action.type) { case ActionTypes.ADD_NUM: return state + action.payload; case ActionTypes.MINUSE_NUM: return state - action.payload default: return state; } }; // const store = createStore(numReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && // window.__REDUX_DEVTOOLS_EXTENSION__()); const store = createStore(numReducer, composeWithDevTools(applyMiddleware(thunk, redudxPromise), // other store enhancers if any )); export default store;
組件代碼:
import React, { Component } from 'react'; import { connect } from 'react-redux'; import { ActionCreators } from '../store'; function mapStateToProps(state) { return { Num: state }; } function mapDispatchToProps(dispatch) { return { AddNum: (num) => dispatch(ActionCreators.AddNum(num)), MinusNum: (num) => dispatch(ActionCreators.MinusNum(num)), AddNumAsync: (num) => dispatch(ActionCreators.AddNumAsync(num)), MinusNumAsyn: (num) => dispatch(ActionCreators.MinusNumAsyncPromise(num)) }; } class RRDemo extends Component { render() { return ( <div> { this.props.Num } <hr/> <button onClick={ () => this.props.AddNum(1)}> +1 </button> <button onClick={() => { this.props.AddNumAsync(2) .then(res => console.log(res)) .catch(e => console.log(e)) }}> async+1 </button> <button onClick={() => { this.props.MinusNum(1) }}> -1 </button> <button onClick={() => { this.props.MinusNumAsyn(1) .then(res => console.log(res)) }}> async Promise -1 </button> </div> ); } } export default connect( mapStateToProps, mapDispatchToProps )(RRDemo);
redux-devtools-extension 調試工具

安裝瀏覽器插件
- chrome 安裝插件
地址: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
- firefox 安裝插件
地址: https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/
基本使用
創建store的時候,添加如下代碼
const store = createStore( reducer, /* preloadedState, */ + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
redux-devtools-extension輔助配置調試插件
安裝:
npm install --save-dev redux-devtools-extension
import { createStore, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; const store = createStore(reducer, composeWithDevTools( applyMiddleware(...middleware), // other store enhancers if any ));