react-redux的使用從action規劃到reducer實現及完整案例
網路中介紹redux和react-redux的文章非常非常多我為什麼要寫這篇文章呢?因為他們都寫得不好。好與不好的標準就是對於一個需要學習react redux的人能不能在讀完文章後順利理解和完成可執行案例。由於時間關係和演示專案對檔案命名不是很合理,請諒解。 redux作為一個狀態管理庫其實是獨立的。可以在angular,vue,react或者jQuery中使用。當然使用redux你需要遵循一定的規律或者標準,這會在專案和程式碼中體現。
規劃結構
在本案例中我要做一個使用者管理的功能,用redux來管理使用者的資料。規劃的結構如下:
{ users:[ { name: 'xxx', gender: 'm' }, { name: 'yyy', gender: 'f' }, ], selectedUser: { name: '魏永強', gender: 'm' } }
這一步非常重要,你需要先自己構思下你的應用的單一狀態樹。這個結構請大家記住了後邊在構建action, reducer時你就會覺得非常清晰
;
功能定位
- 我們需要可以新增使用者
ADD_USER
- 我們需要在列表中點選檢視使用者詳情
SELECT_USER
- 我們需要雙擊列表中資料刪除該使用者
DELETE_USER
用程式碼描述功能[action編寫
]
redux官網對action的描述是:把資料從應用傳入store的有效載荷。 其實這個解釋很難理解,我覺得在我們開發的視角應該覺得action是對功能的描述。
action.js檔案
export const ADD_USER = "ADD_USER" ;
export const SELECT_USER = "SELECT_USER";
export const DELETE_USER = "DELETE_USER";
export function addUser(user) {
return {
type: ADD_USER,
payload: user
};
};
export function selectUser(user) {
return {
type: SELECT_USER,
payload: user
};
};
export function deleteUser( id) {
return {
type: DELETE_USER,
payload: id
};
};
注意每個物件都有一個type就是我們的功能描述和載荷payload其實就是我們的功能需要的資料
用程式碼實現功能[reducer編寫
]
redux官方的解釋是:指定了應用狀態的變化如何響應 actions 併發送到 store 的
也就是實現action對功能的描述
這兒我們要思考下怎麼樣來劃分reducer了。回到文章剛開始規劃結構
我們知道本應用就涉及到兩個資料一個是users也就是使用者列表,另一個是selectedUser也就是使用者詳情。那我們就建立兩個js:
reducers/
|----user_list.js
|----user_selected.js
|----index.js
先看程式碼:
user_list.js
/*
@Author: weiyognqiang<[email protected]>
@Date: 2018/10/17
*/
import { ADD_USER, DELETE_USER } from '../actions';
export default function (state = [], action) {
switch (action.type) {
case ADD_USER:
return [...state, action.payload];
case DELETE_USER:
if (action.payload >= state.length) {
return state;
}
state.splice(action.payload, 1);
return [...state];
default:
return state;
}
}
user_selected.js
/*
@Author: weiyognqiang<[email protected]>
@Date: 2018/10/17
*/
import { SELECT_USER } from '../actions';
export default function (state = null, action) {
switch (action.type) {
case SELECT_USER:
return action.payload;
default:
return state;
}
}
index.js
import { combineReducers } from "redux";
import Users from "./user_lists";
import SelectedUser from "./user_selected";
const rootReducer = combineReducers({
users: Users,
selectedUser: SelectedUser
});
export default rootReducer;
注意:
在reducer的處理函式中傳入了state和action。需要注意的是reducer是純函式
那什麼事純函式呢,就是你傳入的引數相同了他返回的結果是確定的。不產生副作用
(其實我覺得這個形容很彆扭)。舉一個老生常談的例子。比如在函式中有Math.Random(), Date.Now()之類的是不行的。因為隨機和與時間相關讓我們對函式的結果沒法預知。
以下兩點非常重要:
- 不要去改變state的值
- 如果有未知的action請一定返回舊的state
注意我在ADD_USER和DELETE_USER的處理,一個要想users陣列物件增加元素,一個要刪除陣列物件的某個值。但是以上兩點必須遵循所以我們用了...物件展開運算子。當然你也可以使用Object.assign({},{},{})深拷貝
以上程式碼對reducer進行了拆分但是在index.js中使用redux提供的combineReducers進行了合併。該函式返回的結果是和combineReducers傳入物件結構完全一致的state,這裡需要注意下。當然你可以自己處理combineReducers但是官方的combineReducers還進行了reducer檢查,比如是否在action未匹配時返回了state等。
構建操作介面[container和component以及connect
]
在我們設計中可以把頁面元素拆分成元件component
但是在真實處理中這也存在問題,就是元件中資料處理邏輯混在一起。所以有些人抽象了container
這個東西。把這個東西成為資料元件,顧名思義就是專門為元件提供資料
。給元件提供資料那麼就是元件間通訊就要使用props。
那麼我們同樣需要兩個container一個用來顯示列表
,一個用來顯示詳情
。
如何讓store中的資料和元件溝通
其實到了這一步很多人發現其實缺了什麼。也就是我們就資料如何進入store的操作描述(action)及操作實現(reducer)都完成了。但是如何在介面使用這些資料呢?
顯然,要讓他們關聯起來。這個就是react-redux提供的connect把store中的資料以及操作跟我們的檢視元件連線起來
這個過程很抽象其實不難理解。
建立元件及連線[connect
]
containers/user-list.js
/*
@Author: weiyognqiang<[email protected]>
@Date: 2018/10/17
*/
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Input, Button, List } from 'antd';
import {selectUser, addUser, deleteUser} from "../actions";
class UserList extends Component{
constructor(props) {
super(props);
}
showUserLists()
{
return this.props.users.map((user, index) => (
<li
onClick={() => this.props.selectUser(user)}
onDoubleClick={() => this.props.deleteUser(index)}
key={index}>{user.name}</li>
));
}
render() {
return (
<div>
<Button onClick={() => this.props.addUser({name: '魏永強', gender: 'f'})}>新增</Button>
<ul>
{this.showUserLists()}
</ul>
</div>
);
}
}
function mapStateToProps(state) {
return {
users: state.users
};
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({
addUser: addUser,
selectUser: selectUser,
deleteUser: deleteUser
}, dispatch)
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserList);
containers/user-detail.js
/*
@Author: weiyognqiang<[email protected]>
@Date: 2018/10/18
*/
import React, { Component } from 'react';
import { connect } from 'react-redux';
class UserDetail extends Component{
constructor(props) {
super(props);
}
render() {
if (!this.props.selectUser) {
return (<p>請單擊使用者姓名檢視詳情:</p>);
}
return (
<div>
<p>name: {this.props.selectUser.name}</p>
<p>gender: {this.props.selectUser.gender}</p>
</div>
);
}
}
function mapStateToProps(state) {
return {
selectUser: state.selectedUser
}
}
export default connect(
mapStateToProps
)(UserDetail);
注意
每個container前面的class元件大家都很容易理解。在元件中需要的資料我們都是使用this.props
屬性來引用。那麼我們必須讓他和我們的store中的資料對應起來。這兒有兩個函式需要注意。
mapStateToProps(state)
:從這個函式的結構我們就能看出來其實這個是將store的state和我們的props對應起來了而已,很好理解。
mapDispatchToProps(dispatch)
:這個就是把dispatch和props對應起來了而已。(注意我通篇沒有講解dispatch這個東西,這個就是把我們的操作描述傳遞給reducer的一個函式而已。當然具體的api請查閱官方手冊
)
完了之後呢把props和元件連線起來:
export default connect()(UserDetail);
在頁面中使用元件(通用)
pages/UserList/index.js
import React, { Component } from 'react';
import UserLists from '../../models/containers/user-list';
import UserDetail from '../../models/containers/user-detail';
export default class UserList extends Component{
constructor(props) {
super(props);
}
render() {
return (
<div>
<UserLists />
<UserDetail />
</div>
);
}
}
讓所有容器元件訪問store
[非常重要
]
以上都做完了還不算晚。我們為了讓所有容器元件訪問store我們要使用react-redux提供的Provider 元件來包裹APP元件。當然如果要結合react-router使用的話還要包裹BrowserRouter或其他。
index.js修改
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './models/reducers'
const store = createStore(rootReducer);
store.subscribe(() => {console.log(store.getState())});
ReactDOM.render(<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();