react课程13-Redux入门

Last updated on September 19, 2024 am

🥳本节课开始学习Redux

一、Redux的定义


Redux 是一个用于管理 JavaScript 应用中状态的开源库,通常与 React 一起使用。Redux 的核心思想是将应用的状态存储在一个全局的单一状态树中,这样应用中的任何组件都可以访问和更新该状态。

  • Redux 的三个核心原则:

    • 单一数据源: 应用的所有状态保存在一个对象树中,并且这个状态对象树是只读的。

    • 状态是只读的: 唯一改变状态的方法是发出一个 action,action 是一个描述事件的普通 JavaScript 对象。

    • 使用纯函数来修改状态: 通过编写纯函数 reducers 来根据 action 描述的事件返回新的状态。

  • 使用 Redux 的步骤:

    • 创建 storecreateStore() 用于创建 Redux 的存储(store)。(含有多个Reducer,为每个应用程序特性或每个数据创建一个reducer)

    • 定义 reducer:reducer 是一个纯函数,接收当前的状态和 action,然后返回新的状态。

    • 分发 action:组件可以通过 dispatch(action) 来发送 action,进而触发状态的变化。

    • 连接组件与 store:通过 connect() 或者 hooks(如 useSelectoruseDispatch)来让 React 组件与 Redux 的状态进行交互。

    image-20240907151053166

(1)什么是纯函数

纯函数(Pure Function)是指在相同的输入下总是返回相同输出,并且没有副作用的函数。

1、相同的输入,得到相同的输出
一个纯函数依赖于它的输入参数,任何时候只要输入相同,输出就一定相同。例如,sum(a, b) 函数总是返回 a + b,不论什么时候调用。

2、没有副作用
纯函数不会修改外部的状态,也不会影响外部环境。比如它不会更改全局变量、修改传入的参数,也不会执行诸如 IO 操作、网络请求等副作用。

3、纯函数的优点

  • 易于测试和调试:由于纯函数依赖于输入和输出,没有副作用,因此它们容易进行单元测试。
  • 可组合性:多个纯函数可以组合使用,减少了复杂度。
  • 可预测性:因为纯函数不会依赖外部状态或产生副作用,它的行为更容易预测。

(2)为什么修改状态不算副作用

副作用指的是对外部环境的改变,例如修改外部的变量、执行 I/O 操作、调用 API 等,这些操作会导致应用的外部环境在函数运行前后发生变化。而 Redux 中的 reducer 是一个纯函数,它接收旧的状态和 action,并返回一个新的状态对象,而不是修改现有的状态。

不可变性:在 Redux 中,reducer 不直接修改传入的状态,而是创建并返回一个新的状态对象。这种方式保证了状态的不可变性,从而避免了副作用。

直接修改状态:

function reducer(state, action) {

state.count += 1;

return state; }

返回新状态:

1
2
3
4
5
6
function reducer(state, action) {
return {
...state, // 创建一个新的状态对象
count: state.count + 1 // 修改新状态的 count 属性
};
}

二、学习Redux

⭕npm install redux


(1)建立Store

1、start

const store = createStore(reducer); //建立store

store.dispatch({ type: "account/deposit", payload: 500 }); //发出动作

console.log(store.getState()); //获取状态值

2、working with action creators

function deposit( amount ) {

 `return { type: "account/deposit", payload: amount };`

}

store.dispatch(deposit(500));

3、两个reducer
1
2
3
4
5
6
const rootReducer = combineReducers({
account: accountReducer,
customer: customerReducer,
});

const store = createStore(rootReducer);
4、state slices(专业的状态结构)

image-20240907170229118

slice文件存储与相关用户有关的状态以及action creactor函数;(export defaut reducer,export action creator 函数)

index.js文件:import store from "./store";

(2)将redux和react连接起来

⭕npm i react-redux


  • 首先导入Providerimport { Provider } from "react-redux"; 😋和context API很类似哦

  • 然后把app包裹进去:

1
2
3
4
5
6
7
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);

接下来每个需要从Redux的store信息的组件都可以接收到了。

  • 读取信息:

    • 导入hook:import { useSelector } from "react-redux";
    • 读取信息:const customer = useSelector( (store) => store.customer ); or

    const customer = useSelector( (store) =>store.customer.fullName (customer与在store.js中的命名对应)

1、dispatching actions from our react app

const dispatch=useDispatch(); 得到dispath函数,这样dispatch就会按照平常那样工作啦,只需要传递所需的参数就可以了

PS:(解构value)(冒号前的是重构前的名字,冒号后是此文件要用的名字)

1
2
3
4
5
const {
loan: currentLoan,
loanPurpose: currntLoanPurpose,
balance,
} = useSelector((store) => store.account);
2、把组件连接到Redux的旧方法

mapStateToProps 是 Redux 中旧的(传统的)方法之一,用于将 Redux 的全局状态映射到 React 组件的 props 上,从而使组件能够访问 Redux 状态。在使用 mapStateToProps 的时候,通常搭配 connect 函数来将 Redux 状态和 React 组件连接起来。

例如:

image-20240907203055337

1️⃣mapStateToProps函数的作用是从Redux的store中提取所需的状态,并通过props将这些状态传递给React组件,它接收两个参数:state、ownProps(可选,组件自身的props)

2️⃣使用connect接收mapStateToProps变成一个新的函数,这个函数接收Balance函数(想要连接的函数)作为参数,将状态传递给Balance组件

3️⃣Balance组件接收参数

(3)Middleware

要引入这个问题,首先要知道Reducer中不能含有像API调用这样的异步操作,那么这些操作应该放在哪里呢🤔?(在组件中分散地fetching data显然不理想 )(而store里显然也不行)

image-20240907205303059

在 Redux 中,middleware(中间件) 是指一个扩展 Redux dispatch 功能的机制,允许你在发出 action 和 reducer 处理该 action 之间插入自定义的逻辑。它的主要作用是拦截 action,执行一些额外的处理,比如日志记录、异步请求、错误处理等。

1、中间件的作用
  • 处理异步操作:Redux 本身只能处理同步的状态更新,而中间件可以让你执行异步操作,比如网络请求。常见的异步中间件有 redux-thunkredux-saga
  • 记录日志:可以在每次 action 被触发时记录日志。
  • 错误处理:在 action 到达 reducer 之前处理错误或异常情况。
  • 自定义扩展:可以在 dispatch 过程中加入自定义的逻辑,比如修改 action、延迟 dispatch 等。
2、中间件的工作流程

Redux 的中间件基本上是一个函数,它接收 store,然后返回一个函数,该函数接收 next,最后返回一个函数处理 action。中间件在每次 action 被派发时会执行,并且可以控制 action 是否传递到下一个中间件或 reducer。

中间件执行顺序如下:

  • dispatch(action) -> 中间件链 -> reducer -> 更新 store

(4)Redux Thunk的应用

⭕npm i redux-thunk


1、进入store.js做如下改动:
  • 添加导入:import thunk from "redux-thunk";
  • 使用applyMiddleware创建store:const store = createStore(rootReducer, applyMiddleware(thunk));

❓为什么导入thunk不需要花括号?

​ 在 JavaScript 中,模块可以有两种类型的导出方式:

默认导出(default export):一个模块只能有一个默认导出,导入时可以使用任意名字,并且不需要花括号。

命名导出(named export):一个模块可以有多个命名导出,导入时必须使用花括号,且必须与导出时的名字匹配。

​ 在 redux-thunk 这个库中,thunk 是默认导出的内容,所以导入时不需要花括号。

​ 尴尬的是不加花括号报错了哈哈哈所以到底是默认还是命名啊……

2、在action creator函数中做异步API调动

要做API调用后返回action,因此先返回一个函数,React看到这个函数就会知道这是thunk,因此会先执行函数中的内容再返回action。

本次App需要用到的API如图所示,是一个钱币转换API,FROM:https://www.frankfurter.app/docs/


➡️fetch(https://api.frankfurter.app/latest?amount=10&from=GBP&to=USD)

image-20240908131920242

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function deposit(amount, currency) {
if (currency === "USD") return { type: "account/deposit", payload: amount };

return async function (dispatch, getState) { //必须定义为异步函数
dispatch({ type: "account/convertingCurrency" }); //想要设定一个Loading界面

//API call
const res = await fetch( `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`);
const data = await res.json();
const converted = data.rates.USD;

//return action
dispatch({ type: "account/deposit", payload: converted });
};


⁉️为什么上面的操作可以直接return action,下面却要dispatch action?

  • 如果是一个普通的同步操作(如你代码中的 if (currency === "USD") 情况),你可以直接返回一个 action 对象,Redux会立即处理这个 action。但在异步操作中(如 API 调用时),你不能直接返回 action,因为 action 的 payload 是异步获取的。此时,返回的不是普通的 action,而是一个异步的函数(即 thunk 函数),这个函数需要等到异步操作完成后再手动派发 action,更新 Redux store。这里的 dispatch 就是用来派发 action 的。

  • 异步操作(如 fetch API)需要时间来完成。如果你在异步操作之前就返回一个 action,Redux 并不知道这个 action 应该在什么时候派发,它也无法等待异步操作的完成。使用 dispatch 可以确保在异步操作(如 API 请求)完成之后再将最终的结果派发给 Redux store。dispatch 是 Redux 中派发 action 的函数,它通知 Redux store 有一个新的 action 被触发,Redux store 会根据这个 action 更新 state。

😲每一次fetch Data后,得到data之后有一个很好的习惯是console.log(data),来查看data的格式,方便运用它

(5)Redux Dev Tools

1、下载Dev ToolsRedux DevTools - Chrome 应用商店 (google.com)
2、npm i redux-devtools-extension

与视频一致,但此时已经有报错了(表明 redux-devtools-extension 版本与当前安装的 redux@5.0.1 版本不兼容。redux-devtools-extension@2.13.9 需要 redux 的版本是 ^3.1.0^4.0.0,而我使用的是 redux@5.0.1,导致了依赖冲突)

因此只能npm install redux-devtools-extension --legacy-peer-deps来忽略 peerDependencies 的冲突,并继续安装

☑️在store.js中修改

import { composeWithDevTools } from "redux-devtools-extension";

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));

3、界面展示

image-20240908143934918

⭕左边可以看到所有动作,并且可以直接jump到所有历史的状态,右边可以看到状态,下面的滑块栏也可以抵达历史动作的状态。

⭕点击该处可以手动dispatch action

image-20240908144145452

(6)Redux Toolkit

image-20240908152201707

✅compatible: 兼容的 ✅boilerplate样板代码 ✅mutate: 可变的

✅immutable: 不可变的 ✅Immer: Immer库(用于简化不可变数据处理的库)

1、creating store with RTK:

npm i @reduxjs/toolkit(同样版本冲突,按照上面的方法安装)


1
2
3
4
5
6
7
8
9
10
11
12
13
import { configureStore } from "@reduxjs/toolkit";

import accountReducer from "./features/account/accountSlice";
import customerReducer from "./features/customers/customerSlice";

const store = configureStore({
reducer: {
account: accountReducer,
customer: customerReducer,
},
});

export default store;
2、改变Slice

介于不想占用太多空间,这里仅给出AccountSlice的示例(以及不用RTK的版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
balance: 0,
loan: 0,
loanPurpose: "",
isLoading: false,
};

const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
deposit(state, action) {
state.balance += action.payload;
state.isLoading = false;
},
withdraw(state, action) {
state.balance -= action.payload;
},
requestLoan: {
prepare(amount, purpose) {
return {
payload: { amount, purpose },
};
},

reducer(state, action) {
if (state.loan > 0) return;
state.balance = state.balance + action.payload.amount;
state.loan = action.payload.amount;
state.loanPurpose = action.payload.purpose;
},
},
payLoan(state) {
state.balance -= state.loan;
state.loan = 0;
state.loanPurpose = "";
},
convertingCurrency(state) {
state.isLoading = true;
},
},
});

export const { withdraw, requestLoan, payLoan } = accountSlice.actions;

export function deposit(amount, currency) {
if (currency === "USD") return { type: "account/deposit", payload: amount };

return async function (dispatch, getState) {
dispatch({ type: "account/convertingCurrency" });
//API call

const res = await fetch(
`https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
);
const data = await res.json();
const converted = data.rates.USD;
//return action

dispatch({ type: "account/deposit", payload: converted });
};
}

export default accountSlice.reducer;

//------------------V1-No RTK-------------------------
/*
const initialStateAccount = {
balance: 0,
loan: 0,
loanPurpose: "",
isLoading: false,
};

export default function accountReducer(state = initialStateAccount, action) {
switch (action.type) {
case "account/deposit":
return {
...state,
balance: state.balance + action.payload,
isLoading: false,
};
case "account/withdraw":
return { ...state, balance: state.balance - action.payload };
case "account/requestLoan":
if (state.loan > 0) return state;
return {
...state,
balance: state.balance + action.payload.amount,
loan: action.payload.amount,
loanPurpose: action.payload.purpose,
};
case "account/payLoan":
return {
...state,
loan: 0,
loanPurpose: "",
balance: state.balance - state.loan,
};

case "account/convertingCurrency":
return { ...state, isLoading: true };
default:
return state;
}
}

export function deposit(amount, currency) {
if (currency === "USD") return { type: "account/deposit", payload: amount };

return async function (dispatch, getState) {
dispatch({ type: "account/convertingCurrency" });
//API call

const res = await fetch(
`https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
);
const data = await res.json();
const converted = data.rates.USD;
//return action

dispatch({ type: "account/deposit", payload: converted });
};
}

export function withdraw(amount) {
return { type: "account/withdraw", payload: amount };
}

export function requestLoan(amount, purpose) {
return { type: "account/requestLoan", payload: { amount, purpose } };
}

export function payLoan() {
return { type: "account/payLoan" };
}
*/

3、使用RTK的优点🔆
  • 代码简洁、减少样板代码

    • RTK 中的 createSlice 自动生成 action types 和 action creators,因此减少了手动定义 action type 和 creator 的代码量。

    • 在不使用 RTK 的版本中,你需要手动编写大量的 switch-case 语句和 action creator,比如 account/depositaccount/withdraw 等。而在 RTK 中,createSlice 自动完成这些任务,简化了 reducer 和 action 的编写。

  • 支持“可变”状态,简化状态更新逻辑

    • RTK 内部使用 Immer, 允许你在 reducer 中编写像变异对象一样的代码,但实际上它并没有真的修改原始状态,而是生成了一个新的状态对象。这意味着:你在 reducer 中“修改”状态对象的字段时,Immer 会追踪这些变更,并创建一个新的状态对象,保持 Redux 的不可变性原则;如果你没有修改状态,Immer 会返回原始状态(不创建新的对象),从而优化性能。
    • 你可以直接修改状态对象,而不需要手动创建状态的深拷贝,这大大简化了状态更新的代码。
  • 内置异步处理和 middleware 支持

    • RTK 提供了异步 action(如 createAsyncThunk)的简化处理,内置了 thunk 中间件,简化了异步数据请求的管理。在你的代码中,deposit 异步处理逻辑依然需要用 thunk,但使用 RTK 时可以通过 createAsyncThunk 或更好地支持异步流。

    • 你可以直接定义异步 action,不需要像传统 Redux 那样额外引入 redux-thunk 或手动创建异步操作。

  • 自动生成 action creators 和 action types

    • RTK 自动生成 action creators,避免了手动定义 action type 的重复劳动。这不仅减少了错误的可能性,还使代码更具可维护性和一致性。
  • 开发工具集成

    • RTK 与 Redux DevTools 以及其他 Redux 开发工具更好地集成,带有默认的优化配置,例如减少手动配置、提供性能优化等。
4、使用RTK的缺点⛈️
  • 学习曲线
    • 尽管 RTK 提供了很多简化的工具,但对于那些已经熟悉传统 Redux 的开发者来说,理解和掌握 RTK 的 API 和设计模式可能需要一定的时间。比如像 createSlicecreateAsyncThunk 这些 API 对老用户来说可能需要一些时间适应。
  • 隐藏的复杂性
    • 虽然 RTK 内部处理了不可变性和异步操作,但它隐藏了一些 Redux 的原理性代码(如状态的不可变性操作、手动定义 actionreducer 的流程)。这对于希望深入了解 Redux 底层实现的开发者来说,可能减少了对框架的深度理解。
    • 在代码中可以体现出来,使用RTK来自动创建action creator时默认只接收一个参数,因此需要用prepare函数来进行修改
  • 与复杂应用的结合
    • 对于一些非常复杂或高度定制的应用,RTK 的封装可能不够灵活,开发者有时可能需要绕过 RTK 的一些默认行为,以适应应用的特定需求。在这些情况下,传统的 Redux 反而可能更灵活。

三、contextAPI+useReducerRedux的区别

image-20240909101414957

image-20240909101339231

1、中间件和插件支持

  • Context API + useReducer:没有 Redux 那种丰富的中间件和插件生态系统,例如 redux-thunkredux-saga 之类的异步操作工具。通常需要手动处理复杂的异步逻辑。
  • Redux:拥有大量中间件和开发工具(如 Redux DevTools和Middleware),可以很方便地调试和处理异步操作

2、状态存储方式

  • Context API + useReducer:没有全局的单一状态树。通过 useReducer 来管理本地状态,并通过 Context API 提供状态共享的功能。不容易新增状态(要添加新的Provider和Reducer)
  • Redux:有一个全局的单一状态树(store),所有的应用状态都存储在这一个 store 中。这使得状态管理更加集中。创建新的状态slice较为方便

3、起步

  • Context API + useReducer:由React直接提供,很容易建立
  • Redux:需要提供插件支持,在初始建立的时候较为复杂

4、优化(具体见上一节)

  • Context API + useReducer:优化较为复杂
  • Redux:提供了很多开箱即用的优化

react课程13-Redux入门
http://example.com/2024/09/07/react课程13-Redux入门/
Author
Yaodeer
Posted on
September 7, 2024
Licensed under