React
具有着巨大的生态,衍生了许多的状况办理方案。React的状况能够依照两种方式去分类:
可访问性
- 部分状况(
local state
) - 大局状况(
global state
)
域
- 长途状况(
remote state
) - 界面状况(
UI state
)
咱们要根据不同的状况选用适宜的办理方案,以到达最佳实践。
部分状况办理
useState
useState
是咱们最常用的hooks之一,它回来一个数组,包括状况和设置状况的函数
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, 方便复用:
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
目标即可
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经典写法
依靠安装:
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本身
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
中
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
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
组件运用即可
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
依靠安装:
npm i @reduxjs/toolkit
运用Redux tookit
无需再写action creator
, 会主动生成在xxxSlice的actions特点中
createSlice
需要传递name特点,初始值和包括reducer函数的reducers目标
默许状况下,reducer函数的action.payload
只承受一个参数, 假如要传递多个参数, 需要写成目标的方式, 运用prepare
函数回来payload
目标, 此刻便能在reducer函数中运用
accountSlice.js
这儿我复用了经典写法中的deposit函数,说明是彻底兼容经典写法的
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
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, 比较经典写法要简略许多
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
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
)