Redux 进阶
6.1 UI 组件和容器组件
UI 组件负责渲染,容器组件负责页面逻辑。
UI 只负责页面的组件 (傻瓜组件)。
容器组件里的代码都是业务逻辑代码 (聪明组件)。
示例:
// TodoList.js 只负责业务逻辑
import React, { Component } from "react";
import "antd/dist/antd.css";
import store from "./store";
import {
getInputChangeAction,
getBtnClickAction,
getItemDeleteAction,
} from "./store/actionCreators";
import TodoListUI from "./TodoListUI.jsx"
export default class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
// 订阅了 store 的改变。store 改变一次,该方法将会执行一次。
store.subscribe(this.handleStoreChange);
}
handleStoreChange() {
// 用 store 里获取的数据来替换原本的数据
this.setState(store.getState());
}
handleInputChange(e) {
store.dispatch(getInputChangeAction(e.target.value));
}
handleBtnClick() {
store.dispatch(getBtnClickAction());
}
handleItemDelete(index) {
store.dispatch(getItemDeleteAction(index));
}
render() {
return (
<TodoListUI inputValue={this.state.inputValue}
list={this.state.list}
handleInputChange={this.handleInputChange}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>
);
}
}
// TosoListUI.js 只负责渲染
import React, { Component } from 'react'
import { Button, Input, List } from "antd";
export default class TodoListUI extends Component {
render() {
return (
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
<Input
value={this.props.inputValue}
placeholder="todo info"
style={{ width: "300px", marginRight: "10px" }}
onChange={this.props.handleInputChange}
/>
<Button type="primary" onClick={this.props.handleBtnClick}>
提交
</Button>
<List
style={{ marginTop: "10px", width: "300px" }}
bordered
dataSource={this.props.list}
renderItem={(item, index) => (
<List.Item onClick={(index) => {this.props.handleItemDelete(index)}}>
{item}
</List.Item>
)}
/>
</div>
)
}
}
6.2 优化:无状态组件
里边没有 state 只有 render,就称之为无状态组件,直接用函数返回就行。如上面的 TodoListUI 组件,就是无状态组件。无状态组件的优势:性能高,就是一个函数直接返回。相比较于类组件,类组件生成的对象里面有声明周期等东西。
import React from "react";
import { Button, Input, List } from "antd";
export default function TodoListUI(props) {
return (
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
<Input
value={props.inputValue}
placeholder="todo info"
style={{ width: "300px", marginRight: "10px" }}
onChange={props.handleInputChange}
/>
<Button type="primary" onClick={props.handleBtnClick}>
提交
</Button>
<List
style={{ marginTop: "10px", width: "300px" }}
bordered
dataSource={props.list}
renderItem={(item, index) => (
<List.Item
onClick={(index) => {
props.handleItemDelete(index);
}}
>
{item}
</List.Item>
)}
/>
</div>
);
}
6.3 Redux 中发送异步请求获取数据
比如要通过 Ajax 初始化 todo list 列表的流程:先通过挂载的周期函数发送 Ajax 请求,然后将返回的结果打包进 action 里边发送给 store,store 把旧 state 和 action 传给 reducer,reducer 判断 actionType 来进行相应的数据处理,返回新 state 给 store 进行替换。
// TodoList.js
componentDidMount() {
axios.get("/api/todolist").then((res) => {
const data = res.data;
const action = initListAction(data);
store.dispatch(action);
});
}
// actionCreators.js
export const initListAction = (data) => ({
type: INIT_LIST_ACTION,
data,
});
// reducers.js
if (action.type === INIT_LIST_ACTION) {
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.data;
return newState;
}
6.4 使用 Redux-thunk 中间件进行 ajax 请求发送
复杂的逻辑全放在组件里的时候会造成组件维护难度增加。Redux-thunk 可以解决这个问题。
- 安装 redux-thunk:
npm install redux-thunk
- 引用 applyMiddleware,使得可以构建和使用中间件。thunk 和 redux devtools 搭配使用。这里可以看出,redux devtools 也是一种中间件。
// store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";
const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const enhancer = composeEnhancers(applyMiddleware(thunk));
export default createStore(reducer, enhancer);
- 异步代码从组件函数里转移到 actionCreator 里边。使用 redux-thunk 以后,action 可以返回一个函数而不仅仅是一个对象。action 函数 dispatch 给 store 后,store 发现是个函数,该函数便可即时运行。同时,函数从生命周期中抽离,方便自动化测试。
// TodoList.js
componentDidMount() {
store.dispatch(getTodoList());
}
// actionCreator.js
export const initListAction = (data) => ({
type: INIT_LIST_ACTION,
data,
});
export const getTodoList = () => {
// 当 action 为函数的时候,可以接收到 dispatch 方法
return (dispatch) => {
// 异步发送初始化列表的 action
axios.get("/api/todolist").then((res) => {
const data = res.data;
// 获取 action
const action = initListAction(data);
// 发送给 store
dispatch(action);
});
}
}
// reducer.js
if (action.type === INIT_LIST_ACTION) {
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.data;
return newState;
}
6.5 什么是 Redux 中间件
Redux 中间件在 Action 和 Store 之间。
中间件就是对 Dispatch 做了一层封装。
6.6 Redux-saga 中间件入门
thunk 已经很好用了,但是 Redux-saga 做了进一步的划分,将异步方法放在单独一个文件里。
-
安装 redux-saga
npm install redux-saga
-
创建中间件并挂载在 store 上,并创建 sagas.js,里边放异步的方法
import { createStore, compose, applyMiddleware } from "redux"; import reducer from "./reducer"; import createSagaMiddleware from "redux-saga"; // create the saga middleware const sagaMiddleware = createSagaMiddleware(); const composeEnhancers = typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware)); // mount it on the Store export default createStore(reducer, enhancer);
-
正常方式写 action
// TodoList.js componentDidMount() { store.dispatch(getInitList()); }
// actionCreators.js export const getInitList = () => ({ type: GET_INIT_LIST, });
-
在 sagas 里边写异步方法
运行原理:当 action dispatch 的时候,不仅仅 reducer 能获取到 action,saga 里的函数也能接受到,根据不同的 action 来运行不同的函数。
import { takeEvery, put } from "redux-saga/effects"; import { GET_INIT_LIST } from "./actionTypes"; import { initListAction } from "./actionCreators"; import axios from "axios"; // 需要了解迭代器和生成器 function* getInitList() { try { const res = yield axios.get("/api/todolist"); const action = initListAction(res.data); yield put(action); } catch (error) { console.log("list.json 网络请求失败"); } } // generator 函数 function* mySaga() { // 只要接收到了 GET_INIT_LIST,就会触发 getInitList 这个方法 yield takeEvery(GET_INIT_LIST, getInitList); } export default mySaga;
6.7 React-redux 的使用
更方便地在 React 中使用 redux
-
安装 react-redux
npm install react-redux
-
index.js 里引入 Provider 并包裹组件标签,并引入 store。Provider 作用:提供 store 给子组件后,子组件都能获得 store 里的内容。
import React from "react"; import ReactDOM from "react-dom"; import reportWebVitals from "./reportWebVitals"; import TodoList from "./TodoList"; import { Provider } from "react-redux"; import store from "./store" ReactDOM.render( <Provider store={store}> <TodoList /> </Provider>, document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
-
使用 connect 方法获取 store 里的数据
// TodoList.jsx // TodoList 和 store 做连接 export default connect(null, null)(TodoList);
-
编写映射规则1:实现 store 里的数据和 this.state 的映射
class TodoList extends Component { render() { return ( <div> <div> <input type="text" value={this.props.inputValue} /> <button>提交</button> </div> <ul> <li>item1</li> </ul> </div> ); } } // store 变成组件里的 props 的映射规则 const mapStateToProps = (state) => { return { // 组件里的 props.inputValue 映射到 store 里的 inputValue inputValue: state.inputValue, }; } // TodoList 和 store 做连接 export default connect(mapStateToProps, null)(TodoList);
-
编写映射规则2:实现 store 里 dispatch 和 props.方法的映射
import React, { Component } from "react"; import { connect } from "react-redux"; class TodoList extends Component { render() { return ( <div> <div> <input type="text" value={this.props.inputValue} onChange={this.props.changeInputValue} /> <button>提交</button> </div> <ul> <li>item1</li> </ul> </div> ); } } // store 里存储的 state 变成组件里的 props 的规则 const mapStateToProps = (state) => { return { // 组件里的 props.inputValue 映射到 store 里的 inputValue inputValue: state.inputValue, }; }; // store.dispatch 映射到 props 的方法上 const mapDispatchToProps = (dispatch) => { return { changeInputValue(e) { const action = { type: "change_input_value", value: e.target.value, }; dispatch(action); }, }; }; // TodoList 和 store 做连接 export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
-
在 reducer 里接收传递过来的 action,进行相应的处理
const defaultState = { inputValue: "", list: [], }; const reducer = (state = defaultState, action) => { if (action.type === "change_input_value") { const newState = JSON.parse(JSON.stringify(state)); newState.inputValue = action.value; return newState; } return state; }; export default reducer;
至此,输入框便可顺利地通过键盘输入。
6.8 使用 React-redux 完成 TodoList 功能
6.8.1 添加提交功能
// TodoList.jsx
import React, { Component } from "react";
import { connect } from "react-redux";
class TodoList extends Component {
render() {
return (
<div>
<div>
<input
type="text"
value={this.props.inputValue}
onChange={this.props.changeInputValue}
/>
<button onClick={this.props.handleClick}>提交</button>
</div>
<ul>
{/* 渲染列表 */}
{this.props.list.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
);
}
}
// store 里存储的 state 变成组件里的 props 的规则
const mapStateToProps = (state) => {
return {
// 组件里的 props.inputValue 映射到 store 里的 inputValue
inputValue: state.inputValue,
// 获取 store 里的 list
list: state.list,
};
};
// store.dispatch 映射到 props 的方法上
const mapDispatchToProps = (dispatch) => {
return {
...
// 添加 item 操作
handleClick() {
const action = {
type: "add_item",
};
dispatch(action);
},
};
};
// TodoList 和 store 做连接
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
// reducer.js
...
const reducer = (state = defaultState, action) => {
...
if (action.type === "add_item") {
const newState = JSON.parse(JSON.stringify(state));
// 输入框的值添加到列表
newState.list.push(newState.inputValue);
// 清空输入框内容
newState.inputValue = "";
return newState;
}
return state;
};
export default reducer;
6.8.2 点击删除功能实现
import React, { Component } from "react";
import { connect } from "react-redux";
class TodoList extends Component {
render() {
return (
<div>
<div>
<input
type="text"
value={this.props.inputValue}
onChange={this.props.changeInputValue}
/>
<button onClick={this.props.handleClick}>提交</button>
</div>
<ul>
{/* 渲染列表 */}
{this.props.list.map((item, index) => {
return (
<li
key={index}
onClick={(index) => this.props.handleDelete(index)}
>
{item}
</li>
);
})}
</ul>
</div>
);
}
}
// store 里存储的 state 变成组件里的 props 的规则
const mapStateToProps = (state) => {
return {
// 组件里的 props.inputValue 映射到 store 里的 inputValue
inputValue: state.inputValue,
// 获取 store 里的 list
list: state.list,
};
};
// store.dispatch 映射到 props 的方法上
const mapDispatchToProps = (dispatch) => {
return {
// 输入的值存储到 store
changeInputValue(e) {
const action = {
type: "change_input_value",
value: e.target.value,
};
dispatch(action);
},
// 添加 item 操作
handleClick() {
const action = {
type: "add_item",
};
dispatch(action);
},
// 点击 item 删除 item
handleDelete(index) {
const action = {
type: "delete_item",
index,
};
dispatch(action);
},
};
};
// TodoList 和 store 做连接
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
const defaultState = {
inputValue: "",
list: [],
};
const reducer = (state = defaultState, action) => {
if (action.type === "change_input_value") {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
if (action.type === "add_item") {
const newState = JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.inputValue = "";
return newState;
}
if (action.type === "delete_item") {
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState;
}
return state;
};
export default reducer;
TodoList 的基本功能到此全部实现。可以看到 react-redux 的优点在于,组件使用了 connect 的方法进行映射,使得在 store 里的数据发生改变的时候,组件里的获取数据便可跟着改变,而不需要以前 subscribe 来获取数据。
connect 的理解:
// TodoList 和 store 做连接
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
可以看到,TodoList 组件文件导出的并非组件自身,而是 connect 方法运行返回的结果,TodoList 只是 UI 组件,负责显示,connect 把 UI 组件和其他业务逻辑相结合,便组合成了容器组件。
6.8.2 后边需要做的事。。。
actionCreator,actionTypes 等等优化代码可维护性。
使用解构赋值解构 this.state 和 this.props 可以使得代码更优雅。
TodoList 可以写成无状态组件(因为并没有 state,都是从 store 里获取的 state)
完整代码:
// TodoList.jsx
import React from "react";
import { connect } from "react-redux";
import {
getAddItemAction,
getChangeInputValueAction,
getDeleteAction,
} from "./store/actionCreator";
const TodoList = (props) => {
const { inputValue, list, changeInputValue, handleClick, handleDelete } =
props;
return (
<div>
<div>
<input type="text" value={inputValue} onChange={changeInputValue} />
<button onClick={handleClick}>提交</button>
</div>
<ul>
{/* 渲染列表 */}
{list.map((item, index) => {
return (
<li key={index} onClick={(index) => handleDelete(index)}>
{item}
</li>
);
})}
</ul>
</div>
);
};
// store 里存储的 state 变成组件里的 props 的规则
const mapStateToProps = (state) => {
return {
// 组件里的 props.inputValue 映射到 store 里的 inputValue
inputValue: state.inputValue,
// 获取 store 里的 list
list: state.list,
};
};
// store.dispatch 映射到 props 的方法上
const mapDispatchToProps = (dispatch) => {
return {
// 输入的值存储到 store
changeInputValue(e) {
dispatch(getChangeInputValueAction(e.target.value));
},
// 添加 item 操作
handleClick() {
dispatch(getAddItemAction());
},
// 点击 item 删除 item
handleDelete(index) {
dispatch(getDeleteAction(index));
},
};
};
// TodoList 和 store 做连接
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
// actionCreator.js
import { ADD_ITEM, CHANGE_INPUT_VALUE, DELETE_ITEM } from "./actionTypes";
export const getChangeInputValueAction = (inputValue) => ({
type: CHANGE_INPUT_VALUE,
value: inputValue,
});
export const getAddItemAction = () => ({
type: ADD_ITEM,
});
export const getDeleteAction = (index) => ({
type: DELETE_ITEM,
index,
});
// reducer.js
import { ADD_ITEM, CHANGE_INPUT_VALUE, DELETE_ITEM } from "./actionTypes";
const defaultState = {
inputValue: "",
list: [],
};
const reducer = (state = defaultState, action) => {
if (action.type === CHANGE_INPUT_VALUE) {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
if (action.type === ADD_ITEM) {
const newState = JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.inputValue = "";
return newState;
}
if (action.type === DELETE_ITEM) {
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState;
}
return state;
};
export default reducer;
// actionTypes.js
export const CHANGE_INPUT_VALUE = "change_input_value";
export const ADD_ITEM = "add_item";
export const DELETE_ITEM = "delete_item";