React
拥有着庞大的生态,衍生了许多的状态管理方案。React的状态可以按照两种方式去分类:
可访问性
- 局部状态(
local state
) - 全局状态(
global state
)
域
- 远程状态(
remote state
) - 界面状态(
UI state
)
我们要根据不同的情况选用合适的管理方案,以达到最佳实践。
局部状态管理
useState
useState
是我们最常用的hooks之一,它返回一个数组,包含状态和设置状态的函数
jsx
import { useState } from "react";
function Counter(){
const [count, setCount] = useState(0)
return(
<>
<span>{count}</span>
<button onClick={()=>setCount((count)=>count+1)}>+</button>
</>
)
}
useState
定义的状态只能在组件内部使用,或通过props
传递给子组件,当传递层数过多时,会使代码变得复杂和难以理解(prop drilling
)
useReducer
在某些情况下,只使用useState
去管理状态是不够的
- 当组件拥有大量状态且事件处理程序中涉及到大量
state
的更新时 - 当多个状态更新需要同时发生时(作为对同一事件的回调,如"开始游戏")
- 当更新一个状态依赖于一个或多个其他状态时
在上述情况下使用useState
,代码会非常的冗余,违背了DRY
原则的同时,也会使代码难以阅读。此时,useReducer
可以提供很大的帮助
useReducer
是另一种设置状态的方法,非常适合复杂的状态和相关的状态片段,它将相关的状态片段存储在对象中,通过action.type
批量更新状态
useReducer
有三个相关函数和对象:
reducer
: 纯函数(没有副作用),接受当前状态和操作,并返回下一个状态action
: 描述如何更新状态的对象dispatch
: 通过从事件处理程序"发送"action
到reducer
来触发状态更新的函数(相当于setState
)
顾名思义,useReducer
会将操作state的行为合并,减少操作state的次数
我们可以把useReducer
看做是现实中去银行柜台取钱,前台小姐姐就相当于dispatch
,我们告诉她我们要"取钱以及账户和要取的金额"(action
),然后操作人员会将金额取出并更新账户余额(reducer
)
具体代码放在CodeSandbox上了,感兴趣的可以看一下银行demo
useState VS. useReducer
-
useState
非常适合单个、独立的状态片段(数字、字符串、单个数组等)useReducer
非常适合多个相关的状态和复杂状态(例如,具有许多值的对象和嵌套的对象或数组)
-
useState
更新状态的逻辑直接放在事件处理程序或效果中,分布在一个或多个组件中useReducer
更新状态的逻辑集中在一个位置,与组件解耦:reducer
-
useState
通过调用setState(从useState返回的setter)来更新状态useReducer
通过向reducer分派一个action
来更新状态
-
useState
:命令式更新状态useReducer
:声明式更新状态
如何选择?
全局状态管理
Context API
在Context API
的帮助下, 系统在整个应用程序中传递数据,而无需手动在组件树中传递props
, 它允许我们向整个应用"广播"全局状态
Provider
:赋予所有子组件访问value
的权限value
:我们想要提供的数据(通常是状态和函数)Consumers
: 读取使用Provider
的上下文值的所有组件
如图所示: value
的每一次更新都会导致所有Consumers
的重新渲染
在使用Context API
时, 我们通常将Provider
抽离为单独的文件, 然后导出一个使用context
的hook, 方便复用:
jsx
import { createContext, useContext } from "react";
const PostContext = createContext();
function PostProvider({ children }) {
const [posts, setPosts] = useState(() =>
Array.from({ length: 30 }, () => createRandomPost())
);
const [searchQuery, setSearchQuery] = useState("");
function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}
function handleClearPosts() {
setPosts([]);
}
return
<PostContext.Provider
value={{
posts: searchedPosts,
onAddPost: handleAddPost,
onClearPosts: handleClearPosts,
searchQuery,
setSearchQuery,
}}
>
{children}
</PostContext.Provider>
}
const usePost = () => {
const context = useContext(PostContext);
//在PostProvider外使用context, 得到的是undefined
if (context === undefined)
throw new Error("PostContext was used outside of the PostProvider");
return context;
};
export { PostProvider, usePost };
Consumers
使用全局状态也十分简单, 在确保被Provider
包裹后, 直接调用导出的hook解构value
对象即可
jsx
import { PostProvider, usePost } from "./context/PostContext";
function App() {
return (
<PostProvider>
<Header />
<Main />
<Archive />
<Footer />
</PostProvider>
);
}
function Header() {
const { onClearPosts } = usePost();
return (
<header>
<h1>
<span>⚛️</span>The Atomic Blog
</h1>
<div>
<button onClick={onClearPosts}>Clear posts</button>
</div>
</header>
);
}
function SearchPosts() {
const { searchQuery, setSearchQuery } = usePost();
return (
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search posts..."
/>
);
}
Redux
Redux是一个用来管理全局状态的第三方库, 它是一个独立的库 , 我们可以使用react-redux
库将其集成到React应用程序中。它在概念上类似于使用Context API
+ useReducer
通过Redux,我们将所有全局状态都存储在一个全局可访问的store中,使用"aciton"对状态进行更新
从历史上看,Redux管理全局状态的首选方案。今天,这种情况发生了变化,因为有很多选择。许多应用不再需要Redux,除非它们需要大量的全局UI状态
useReducer VS. Redux
useReducer
Redux
可以看到,Redux相比useReducer的机制十分相似,只是增加了action creator
函数(用于处理副作用),然后将多个reducer集中管理,使状态更新逻辑与应用程序的其余部分分离。
Redux中间件------thunk
Redux中间件(Middleware)位于调度操作和存储之间的函数。允许我们在dispatch
之后,到达store
中的reducer
之前运行代码
thunk是Redux实现异步API调用(或其他异步操作)的中间件
目前Redux有着两种主流写法, 官方推荐使用Redux toolkit, 这里我们从一个银行案例中分析对比两种写法:
需求:
- 账户: 余额, 贷款, 贷款目的, 存(涉及货币转换)取钱
- 消费者: 姓名, ID, 创办时间
Redux经典写法
依赖安装:
js
npm i redux
npm i react-redux
npm i redux-thunk //异步操作中间件
npm i redux-devtools-extension //devtools
与账户相关的操作放在accountSlice.js
中,注意点:
- 默认规定在操作前要加前缀,如账户存钱:
account/deposit
- 为每个操作编写
action creator
函数, 返回对应action操作, 副作用在该函数中执行, 确保Reducer是纯函数 - default不再是抛出一个异常, 而是返回state本身
js
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,
loan: action.payload.amount,
loanPurpose: action.payload.purpose,
balance: state.balance + action.payload.amount,
};
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;
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" };
}
customer相关操作与其类似, 放在customrSlice.js
中
js
const initialStateCustomer = {
fullName: "",
nationalID: "",
createdAt: "",
};
export default function customerReducer(state = initialStateCustomer, action) {
switch (action.type) {
case "customer/createCustomer":
return {
...state,
fullName: action.payload.fullName,
nationalID: action.payload.nationalID,
createdAt: action.payload.createdAt,
};
case "customer/updateName":
return {
...state,
fullName: action.payload,
};
default:
return state;
}
}
export function createCustomer(fullName, nationalID) {
return {
type: "customer/createCustomer",
payload: { fullName, nationalID, createdAt: new Date().toISOString() },
};
}
export function updateName(fullName) {
return { type: "customer/updateName", payload: fullName };
}
在store.js
中, 我们将两个reducer合并为一个, 创建store并应用上中间件和devtools
js
import { applyMiddleware, combineReducers, createStore } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import accountReducer from "./features/accounts/accountSlice";
import customerReducer from "./features/customers/customerSlice";
const rootReducer = combineReducers({
account: accountReducer,
customer: customerReducer,
});
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
export default store;
最后在index.js
中使用Provider
组件应用即可
jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Redux toolkit写法
Redux tookit
是编写Redux代码的现代和首选方式, 它100%兼容Redux经典写法,允许我们一起使用它们, 我们可以通过Redux tookit
编写更少的代码来实现相同的结果。
相比经典写法,Redux tookit
主要有三个不同点
- 我们可以在reducer内部编写"改变"状态的代码(将通过"Immer"库在幕后转换为不可变逻辑)
- 自动创建
action creator
- 自动设置
thunk
中间件和DevTools
依赖安装:
js
npm i @reduxjs/toolkit
使用Redux tookit
无需再写action creator
, 会自动生成在xxxSlice的actions属性中
createSlice
需要传递name属性,初始值和包含reducer函数的reducers对象
默认情况下,reducer函数的action.payload
只接受一个参数 , 如果要传递多个参数, 需要写成对象的形式, 使用prepare
函数返回payload
对象, 此时便能在reducer函数中使用
accountSlice.js
这里我复用了经典写法中的deposit函数,说明是完全兼容经典写法的
js
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.loan = action.payload.amount;
state.loanPurpose = action.payload.purpose;
state.balance += action.payload.amount;
},
},
payLoan(state) {
state.balance -= state.loan;
state.loan = 0;
state.loanPurpose = "action.payload.purpose";
},
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;
dispatch({ type: "account/deposit", payload: converted });
};
}
export default accountSlice.reducer;
createSlice.js
js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
fullName: "",
nationalID: "",
createdAt: "",
};
const customerSlice = createSlice({
name: "customer",
initialState,
reducers: {
createCustomer: {
prepare(fullName, nationalID) {
return {
payload: {
fullName,
nationalID,
createdAt: new Date().toISOString(),
},
};
},
reducer(state, action) {
state.fullName = action.payload.fullName;
state.nationalID = action.payload.nationalID;
state.createdAt = action.payload.createdAt;
},
},
updateName(state, action) {
state.fullName = action.payload;
},
},
});
export const { createCustomer, updateName } = customerSlice.actions;
export default customerSlice.reducer;
store.js
redux-toolkit会自动设置thunk中间件和Devtools, 相比经典写法要简单许多
js
import { configureStore } from "@reduxjs/toolkit";
import accountReducer from "./features/accounts/accountSlice";
import customerReducer from "./features/customers/customerSlice";
const store = configureStore({
reducer: {
account: accountReducer,
customer: customerReducer,
},
});
export default store;
使用和更新store中的状态
redux给我们提供了两个hooks用于使用和更新状态useSelector
和useDispatch
注意, 我们更新状态时, 不能直接传入action, 而是调用action creator
函数返回的action
js
import { useDispatch, useSelector } from "react-redux";
import { deposit, withdraw, requestLoan, payLoan } from "./accountSlice";
const dispatch = useDispatch();
//选择account(name属性)
const account = useSelector((store) => store.account);
function handlePayLoan() {
if (account.loan === 0) return;
//dispatch接收返回action的函数
dispatch(payLoan());
}
案例完整代码已上传github,欢迎查阅
Redux的优势
相比于Context API + useReducer
, Redux无需手动优化, 它是开箱即用的, Immer.js这个库会在幕后帮我们进行优化, 而使用Context我们需要去考虑优化问题(memo
、useMemo
、useCallback
)