Redux基本使用
Redux基本使用
什么是Redux?
- Redux是一个管理状态(数据)的容器,提供了可预测的状态管理
- 什么是可预测的状态管理?
- 数据在什么时候,因为什么,发生了什么改变,都是可以控制和追踪的,我们就称之为预测的状态管理
- 为什么要使用Redux?
- React是通过数据驱动界面更新的,React负责更新界面, 而我们负责管理数据
- 默认情况下我们可以在每个组件中管理自己的状态, 但是现在前端应用程序已经变得越来越复杂
- 状态之间可能存在依赖关系(父子、共享等),一个状态的变化会引起另一个状态的变化
- 所以当应用程序复杂的时候, 状态在什么时候改变,因为什么改变,发生了什么改变,就会变得非常难以控制和追踪
- 所以当应用程序复杂的时候,我们想很好的管理、维护、追踪、控制状态时, 我们就需要使用Redux
- Redux核心理念
- 通过 store 来 保存数据
- 通过 action 来 修改数据
- 通过 reducer 来 关联 store 和 action
三大原则
- Redux三大原则
- 单一数据源
- 整个应用程序的state只存储在一个 store 中
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
- State是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
- 使用纯函数来执行修改
- 通过reducer将 旧state和 action联系在一起,并且返回一个新的State:
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
- 什么是纯函数
返回结果只依赖于它的参数,并且在执行过程里面没有副作用
- 什么是纯函数
// 纯函数
function sum(num1, num2){
return num1 + num2;
}
// 非纯函数
let num1 = 10;
function sum(num2){
return num1 + num2;
}
// 纯函数
const num1 = 10;
function sum(num2){
return num1 + num2;
}
基本使用v1
准备工作
- 创建 demo 目录
- cd demo
- npm init -y # 初始化一个node项目
- npm install –save redux # 安装redux
redux的使用
store.subscribe() # 监听函数(一旦 state 发生变化,就自动执行这个函数)
store.getState() # 获取当前的 state
store.dispatch() # 派发 action(用于修改state)
import { createStore } from 'redux'
const initState = {}
const addAction = { type: 'ADD_COUNT', num: 1 }
const subAction = { type: 'SUB_COUNT', num: 1 }
function reducer(state = initState, action) {
switch(action.type) {
case 'ADD_COUNT':
return { count: state.count + action.num };
case 'SUB_COUNT':
return { count: state.count - action.num};
default:
return state
}
}
const store = createStore(reducer)
// 使用 监听state更新 store.subscribe会返回一个Func,这个Func可以用来取消订阅
const unSubscribe = store.subscribe(() => {
console.log(store.getState()) // 获取state中的最新数据
})
// 取消监听
unSubscribe()
// 修改state中的数值
store.dispatch(addAction)
store.dispatch(subAction)
- 当前代码存在的问题
- 1.store、action、reducer代码都写在一个文件中,不利于维护
- 2.action和reducer中都是使用字符串,来指定和判断操作类型, 写错不报错
- 3.action中的操作写死了,不够灵活
优化v2
import redux from 'redux'
const initState = {}
const ADD_COUNT = 'ADD_COUNT'
const SUB_COUNT = 'SUB_COUNT'
const addAction = (num) => {
return { type: 'ADD_COUNT', num: num }
}
const subAction = (num) => {
return { type: 'SUB_COUNT', num: num }
}
function reducer(state = initState, action) {
switch(action.type) {
case ADD_COUNT:
return { count: state.count + action.num };
case SUB_COUNT:
return { count: state.count - action.num};
default:
return state
}
}
const store = redux.createStore(reducer)
// 使用 监听state更新 store.subscribe会返回一个Func,这个Func可以用来取消订阅
const unSubscribe = store.subscribe(() => {
console.log(store.getState()) // 获取state中的最新数据
})
// 取消监听
unSubscribe()
// 修改state中的数值
store.dispatch(addAction)
store.dispatch(subAction)
优化v3 工程化
结合React,在src下创建store文件夹
store/constants.js
// 定义常量
export const ADD_COUNT = 'ADD_COUNT',
export const SUB_COUNT = 'SUB_COUNT'
sotre/index.js
import {createStore} from 'redux';
import reducer from './reducer';
// 利用store来保存状态(state)
const store = createStore(reducer);
export default store;
store/reducer.js
import { ADD_COUNT, SUB_COUNT } from "./constants";
// 初始化状态
const initState = {
count: 0
}
function reducer(state = initState, action) {
switch(action.type) {
case ADD_COUNT:
return { count: state.count + action.num }
case SUB_COUNT:
return { count: state.count - action.num }
default:
return state
}
}
export default reducer
store/actions.js
import { ADD_COUNT, SUB_COUNT } from "./constants";
export const addAction = (num) => {
return { type: ADD_COUNT, num: num }
}
export const subAction = (num) => {
return { type: SUB_COUNT, num: num }
}
/src/About.js
import React, { Component } from 'react'
import store from '../store'
import { addAction, subAction } from '../store/actions'
class About extends Component {
constructor(props) {
super(props)
this.state = {
count: store.getState().count
}
}
componentDidMount() {
this.noSubscribe = store.subscribe(() => {
// 监听store
this.setState({
count: store.getState().count
})
})
}
componentWillUnmount() {
this.noSubscribe() // 移除监听
}
handleAdd = () => {
store.dispatch(addAction(1))
}
render() {
const { count } = this.state
return (
<div>
<h3>About页面</h3>
<p>Count:{count}</p>
<button onClick={this.handleAdd}>增加1</button>
</div>
)
}
}
export default About
src/Test.js
import React, { Component } from 'react'
import store from '../store'
import { subAction } from '../store/actions'
class Test extends Component {
constructor(props) {
super(props)
this.state = {
count: store.getState().count
}
}
componentDidMount() {
store.subscribe(() => {
this.setState({ count: store.getState().count })
})
}
handleSub = () => {
store.dispatch(subAction(1))
}
render() {
const { count } = this.state
return (
<div>
<h3>Test页面</h3>
<p>Count:{count}</p>
<button onClick={this.handleSub}>减少1</button>
</div>
)
}
}
export default Test
- 存在问题:
- 1.冗余代码太多, 每次使用都需要在构造函数中获取
- 2.每次使用都需要监听和取消监听
- 3.操作store的代码过于分散
- 如何优化:
- 如何解决冗余代码太多问题
- 使用 React-Redux
- 什么是 React-Redux
- React-Redux是Redux官方的绑定库,能够让我们在组件中更好的读取和操作Redux 保存的状态
- 如何解决冗余代码太多问题
优化v4 react-redux
npm install --save react-redux
src/About.js
import React, { Component } from 'react'
import { addAction } from '../store/actions'
import { connect } from 'react-redux'
class About extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
const { count } = this.props
return (
<div>
<h3>About页面</h3>
<p>Count:{count}</p>
<button onClick={() => this.props.handleAdd(1)}>增加1</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleAdd(num) {
dispatch(addAction(num))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(About)
src/Test.js
import React, { Component } from 'react'
import { subAction } from '../store/actions'
import { connect } from 'react-redux'
class Test extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
const { count } = this.props
return (
<div>
<h3>Test页面</h3>
<p>Count:{count}</p>
<button onClick={() => this.props.handleSub(1)}>减少1</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSub(num) {
dispatch(subAction(num))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Test)
优化v5 网络请求
Store/constants.js
// 定义常量
export const ADD_COUNT = 'ADD_COUNT'
export const SUB_COUNT = 'SUB_COUNT'
export const SET_INFO = 'SET_INFO'
Store/actions.js
import { ADD_COUNT, SET_INFO, SUB_COUNT } from "./constants";
export const addAction = (num) => {
return { type: ADD_COUNT, num: num }
}
export const subAction = (num) => {
return { type: SUB_COUNT, num: num }
}
export const setInfo = (info) => {
return { type: SET_INFO, payload: info}
}
Store/reducer.js
import { ADD_COUNT, SUB_COUNT, SET_INFO } from "./constants";
// 初始化状态
const initState = {
count: 0,
info: {}
}
function reducer(state = initState, action) {
switch(action.type) {
case ADD_COUNT:
return { ...state, count: state.count + action.num }
case SUB_COUNT:
return { ...state, count: state.count - action.num }
case SET_INFO:
console.log(action)
return { ...state, info: action.payload }
default:
return state
}
}
export default reducer
components/About.js
import React, { Component } from 'react'
import { addAction, setInfo } from '../store/actions'
import { connect } from 'react-redux'
class About extends Component {
constructor(props) {
super(props)
this.state = {}
}
componentDidMount() {
setTimeout(() => {
const info = {
name: '无敌',
age: 18,
address: '中国'
}
this.props.changeInfo(info)
},2000)
}
render() {
const { count, info = {} } = this.props
return (
<div>
<h3>About页面</h3>
<p>Count:{count}</p>
<button onClick={() => this.props.handleAdd(1)}>增加1</button>
<div>个人信息:
<p>名字:{info.name}</p>
<p>年龄:{info.age}</p>
<p>地址:{info.address}</p>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
info: state.info
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleAdd(num) {
dispatch(addAction(num))
},
changeInfo(info) {
dispatch(setInfo(info))
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(About)
当前保存异步数据存在的问题
- 异步数据的获取,可以选择性的放到redux中,而不是生命周期中。当然两者并没有谁对谁错,具体看需求而定!
如何在Redux中 获取网络数据
- 使用 redux-thunk 中间件
redux-thunk 作用
- 默认情况下 dispatch 只能接收一个对象
- 使用 redux-thunk 可以让 dispatch 除了可以接收一个对象以外, 还可以接收一个函数
- 通过 dispatch 派发一个函数的时候能够去执行这个函数, 而不是执行 reducer 函数
redux-thunk 如何使用
安装 redux-thunk
npm install --save redux-thunk
在创建 store 时应用 redux-thunk 中间件
在 action 中获取网络数据
在组件中派发 action
优化 v6 redux-thunk
store/index.js
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import ThunkMiddleware from 'redux-thunk';
const storeEnhancer = applyMiddleware(ThunkMiddleware)
// 利用store来保存状态(state)
const store = createStore(reducer, storeEnhancer);
export default store;
store/action.js
import { ADD_COUNT, SET_INFO, SUB_COUNT, SET_LOADING } from "./constants";
export const addAction = (num) => {
return { type: ADD_COUNT, num: num }
}
export const subAction = (num) => {
return { type: SUB_COUNT, num: num }
}
export const setInfo = (info) => {
return { type: SET_INFO, payload: info}
}
export const setLoading = (isLoading) => {
return { type: SET_LOADING, payload: isLoading}
}
export const getInfo = (dispatch,getState) => {
dispatch(setLoading(true))
setTimeout(() => {
dispatch(setInfo({
name: '无敌',
age: '999',
address: '花果山'
}))
dispatch(setLoading(false))
},2000)
}
Store/constants.js
// 定义常量
export const ADD_COUNT = 'ADD_COUNT'
export const SUB_COUNT = 'SUB_COUNT'
export const SET_INFO = 'SET_INFO'
export const SET_LOADING = 'SET_LOADING'
components/About.js
import React, { Component } from 'react'
import { addAction, setInfo, getInfo } from '../store/actions'
import { connect } from 'react-redux'
class About extends Component {
constructor(props) {
super(props)
this.state = {}
}
componentDidMount() {}
render() {
const { count, info = {}, loading } = this.props
return (
<div>
<h3>About页面{loading && 'loading...'}</h3>
<p>Count:{count}</p>
<button onClick={() => this.props.handleAdd(1)}>增加1</button>
<div>个人信息:
<p>名字:{info.name}</p>
<p>年龄:{info.age}</p>
<p>地址:{info.address}</p>
</div>
<button onClick={this.props.handleUpdateInfo}>查询用户信息</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
info: state.info,
loading: state.isLoading
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleAdd(num) {
dispatch(addAction(num))
},
changeInfo(info) {
dispatch(setInfo(info))
},
handleUpdateInfo() {
// 默认情况下 dispatch只能接收一个对象
// 如果想让dispatch方法可以接收一个对象以外,还可以接收一个方法 这时我们可以使用redux-thunk
// redux-thunk可以让dispatch接收一个函数,让我们在通过dispatch派发任务时,去执行我们传入的方法
// 在dispatch时执行异步的getInfo方法
dispatch(getInfo)
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(About)
优化 v7 combineReducers
一般比较大的项目中,我们这样做所有的actions,所有的reducer都放到一个文件中还是比较臃肿的,因为我们需要进行按模块化拆分
因此我们可以使用 combineReducers,它的作用是将多个reducer进行整合,合并所有零散的reducer为一个大的reducer,
store/reducers/index.js
import { combineReducers } from 'redux'
import test from './test.js'
import about from './about.js'
const rootReducer = combineReducers({test,about})
export default rootReducer
store/reducers/about.js
import { SET_INFO, SET_LOADING } from "../constants";
// 初始化状态
const initState = {
info: {},
isLoading: false
}
// eslint-disable-next-line import/no-anonymous-default-export
export default function(state = initState, action) {
switch(action.type) {
case SET_INFO:
return { ...state, info: action.payload }
case SET_LOADING:
return { ...state, isLoading: action.payload }
default:
return state
}
}
store/reducers/test.js
import { ADD_COUNT, SUB_COUNT } from '../constants'
let initState = {
count: 0
}
// eslint-disable-next-line import/no-anonymous-default-export
export default function(state = initState, action) {
switch(action.type) {
case ADD_COUNT:
return { ...state, count: state.count + action.num }
case SUB_COUNT:
return { ...state, count: state.count - action.num }
default:
return state
}
}
完整基础demo
目录结构:
代码块
src/components/About.js
import React, { Component } from 'react' import { setInfo, getInfo } from '../store/actions/about' import { connect } from 'react-redux' class About extends Component { getLoadingDom = (str) => { const { loading } = this.props return loading ? 'loading...' : str } render() { const { info = {}, handleUpdateInfo } = this.props return ( <div> <h3>About页面</h3> <div>个人信息: <p>名字:{this.getLoadingDom(info.name)}</p> <p>年龄:{this.getLoadingDom(info.age)}</p> <p>地址:{this.getLoadingDom(info.address)}</p> </div> <button onClick={handleUpdateInfo}>查询用户信息</button> </div> ) } } const mapStateToProps = (state) => { return { info: state.about.info, loading: state.about.isLoading } } const mapDispatchToProps = (dispatch) => { return { changeInfo(info) { dispatch(setInfo(info)) }, handleUpdateInfo() { // 默认情况下 dispatch只能接收一个对象 // 如果想让dispatch方法可以接收一个对象以外,还可以接收一个方法 这时我们可以使用redux-thunk // redux-thunk可以让dispatch接收一个函数,让我们在通过dispatch派发任务时,去执行我们传入的方法 // 在dispatch时执行异步的getInfo方法 dispatch(getInfo) } } } export default connect(mapStateToProps,mapDispatchToProps)(About)
src/components/Test.js
import React, { Component } from 'react' import { subAction } from '../store/actions/test' import { connect } from 'react-redux' class Test extends Component { render() { const { count } = this.props return ( <div> <h3>Test页面</h3> <p>Count:{count}</p> <button onClick={() => this.props.handleSub(1)}>减少1</button> </div> ) } } const mapStateToProps = (state) => { return { count: state.test.count } } const mapDispatchToProps = (dispatch) => { return { handleSub(num) { dispatch(subAction(num)) } } } export default connect(mapStateToProps,mapDispatchToProps)(Test)
src/store/actions/about.js
import { SET_INFO, SET_LOADING } from "../constants"; export const setInfo = (info) => { return { type: SET_INFO, payload: info} } export const setLoading = (isLoading) => { return { type: SET_LOADING, payload: isLoading} } export const getInfo = (dispatch,getState) => { dispatch(setLoading(true)) setTimeout(() => { dispatch(setInfo({ name: '无敌', age: '999', address: '花果山' })) dispatch(setLoading(false)) },2000) }
src/store/actions/test.js
import { ADD_COUNT, SUB_COUNT } from "../constants"; export const addAction = (num) => { return { type: ADD_COUNT, num: num } } export const subAction = (num) => { return { type: SUB_COUNT, num: num } }
src/store/reducers/about.js
import { SET_INFO, SET_LOADING } from "../constants"; // 初始化状态 const initState = { info: {}, isLoading: false } // eslint-disable-next-line import/no-anonymous-default-export export default function(state = initState, action) { switch(action.type) { case SET_INFO: return { ...state, info: action.payload } case SET_LOADING: return { ...state, isLoading: action.payload } default: return state } }
src/store/reducers/test.js
import { ADD_COUNT, SUB_COUNT } from '../constants' let initState = { count: 0 } // eslint-disable-next-line import/no-anonymous-default-export export default function(state = initState, action) { switch(action.type) { case ADD_COUNT: return { ...state, count: state.count + action.num } case SUB_COUNT: return { ...state, count: state.count - action.num } default: return state } }
src/store/reducers/index.js
import { combineReducers } from 'redux' // 自动检索reducers文件夹,导入除index以外的其他文件 const modulesFiles = require.context('./',true,/[^index]\.js$/) const modules = modulesFiles.keys().reduce((modules,modulePath) => { const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/,'$1') const value = modulesFiles(modulePath) modules[moduleName] = value.default return modules }, {}) const rootReducer = combineReducers(modules) export default rootReducer
src/store/Constants.js
// 定义常量 export const ADD_COUNT = 'ADD_COUNT' export const SUB_COUNT = 'SUB_COUNT' export const SET_INFO = 'SET_INFO' export const SET_LOADING = 'SET_LOADING'
src/store/index.js
import { createStore, applyMiddleware } from 'redux'; import reducer from './reducers/index'; import ThunkMiddleware from 'redux-thunk'; const storeEnhancer = applyMiddleware(ThunkMiddleware) // 利用store来保存状态(state) const store = createStore(reducer, storeEnhancer); export default store;
src/App.js
import React, { Component } from 'react' import './App.css'; import About from './components/About' import Test from './components/Test' import { HashRouter as Router, Routes, Route, Link } from 'react-router-dom' class App extends Component { render() { return ( <div className="App"> <Router> <Link to={'/about'}>Go About Page</Link> | <Link to={'/test'}>Go Test Page</Link> <Routes> <Route path='/' element={<About />}></Route> <Route path='/about' element={<About />}></Route> <Route path='/test' element={<Test />}></Route> </Routes> </Router> </div> ); } } export default App;
src/index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { Provider } from 'react-redux'; import store from './store' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <React.StrictMode> <App /> </React.StrictMode> </Provider> ); // 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();
package.json
{ "name": "react-study", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.2", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "redux": "^4.2.0", "redux-thunk": "^2.4.1", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!