redux
Redux 是一个用于可预测和可维护的全局状态管理的 JS 库。
安装依赖
安装依赖@reduxjs/toolkit
和react-redux
到项目中。
javascript
npm install @reduxjs/toolkit react-redux
react-redux
react-redux
是React官方的Redux绑定库,由 Redux 团队直接维护的。
react-redux
在内部实现了许多性能优化,以便你编写的组件仅在实际需要时重新渲染。
此外,通过在 React 组件树中连接多个组件,你可以确保每个连接的组件仅从该组件所需的 store state 中提取特定的数据。 这意味着你自己的组件将需要更少的重新渲染,因为大多数时候这些特定的数据都没有改变。
@reduxjs/toolkit
@reduxjs/toolkit
(Redux工具包,简称 "RTK"),是官方推荐的 Redux 逻辑编写方法。@reduxjs/toolkit
包围绕核心 redux
包,并包含对于构建 Redux 应用至关重要的 API 方法和常见依赖。Redux Toolkit 构建了官方建议的最佳实践,简化了大多数 Redux 任务,防止常见错误,并使编写 Redux 应用变得更加容易。
使用
创建store.ts
文件
typescript
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
},
});
创建 store 后,我们可以通过在 src/main.tsx
中的应用程序外层放置一个 React Redux <Provider>
来使其对我们的 React 组件可用。导入我们刚刚创建的 Redux store,在 <App>
的外层放置一个 <Provider>
,并将 store 作为 prop 传递:
tsx
import { createRoot } from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import router from "./route";
import { Provider } from "react-redux";
import store from "./store";
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
创建 Redux State Slice
typescript
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export const userSlice = createSlice({
name: "user", // 字符串名称标识
initialState: {
age: 0,
name: "",
},
reducers: {
changeAge: (state, action: PayloadAction<number>) => {
state.age = action.payload;
},
changeName: (state, action: PayloadAction<string>) => {
state.name = action.payload;
},
},
});
export const { changeAge, changeName } = userSlice.actions;
export default userSlice.reducer;
添加 Slice Reducers 到 Store
typescript
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./user";
const store = configureStore({
reducer: {
user: userReducer,
},
});
// 从 store 本身推断 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
在组件中使用,用hooks
的写法,我们可以使用 useSelector
从 store 中读取数据,并使用 useDispatch
dispatch actions。
我们创建A,B两个组件,A组件负责展示,B组件负责修改。
tsx
// A组件
import { useSelector } from "react-redux";
export default function AComponent() {
const userName = useSelector((state) => state.user.name);
const userAge = useSelector((state) => state.user.age);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{userName}</div>
<div>用户年龄:{userAge}</div>
</div>
);
}
这时候发现有ts的类型问题
我们可以创建 useDispatch
和 useSelector
hooks 类型化版本以供在应用程序中使用。
typescript
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import userReducer from "./user";
const store = configureStore({
reducer: {
user: userReducer,
},
});
// 从 store 本身推断 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 创建 `useDispatch` 和 `useSelector` hooks 类型化版本
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export default store;
修改A组件
tsx
// A组件
import { useAppSelector } from "@/store/index";
export default function AComponent() {
const userName = useAppSelector((state) => state.user.name);
const userAge = useAppSelector((state) => state.user.age);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{userName}</div>
<div>用户年龄:{userAge}</div>
</div>
);
}
tsx
// B组件
import { useAppDispatch } from "@/store/index";
import { changeAge, changeName } from "@/store/user";
export default function BComponent() {
const dispatch = useAppDispatch();
const changeUserName = (val: string) => {
dispatch(changeName(val));
};
const changeUserAge = (val: string) => {
dispatch(changeAge(Number(val)));
};
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input
type="text"
onChange={(e) => changeUserName(e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => changeUserAge(e.currentTarget.value)}
/>
</div>
</div>
);
}
将两个组件一起引入同一页面演示一下
tsx
import "./index.scss";
import AComponent from "./AComponent";
import BComponent from "./BComponent";
export default function Demo() {
return (
<div className="wrap">
<div className="component-wrap">
<AComponent></AComponent>
</div>
<div className="component-wrap">
<BComponent></BComponent>
</div>
</div>
);
}
zustand
基于 Flux
模型实现的小型、快速和可扩展的状态管理解决方案,拥有基于 hooks
的舒适的API,非常地灵活且有趣。
安装依赖
javascript
npm install zustand
使用
Store 初始化
用 create 函数创建一个 store,创建的 store 是一个 hook
,你可以放任何东西到里面:基础变量,对象、函数,状态必须不可改变地更新,set
函数合并状态以实现状态更新。
typescript
import { create } from "zustand";
interface UserState {
name: string;
age: number;
changeName: (val: string) => void;
changeAge: (val: number) => void;
}
export const useUserStore = create<UserState>((set) => ({
name: "",
age: 0,
changeName: (value: string) => set(() => ({ name: value })),
changeAge: (value: number) => set({ age: value }),
}));
Store 绑定组件
tsx
// A组件
import { useUserStore } from "@/store/user";
export default function AComponent() {
const userName = useUserStore((state) => state.name);
const userAge = useUserStore((state) => state.age);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{userName}</div>
<div>用户年龄:{userAge}</div>
</div>
);
}
tsx
// B组件
import { useUserStore } from "@/store/user";
export default function BComponent() {
const changeUserName = useUserStore((state) => state.changeName);
const changeUserAge = useUserStore((state) => state.changeAge);
const ageInputChange = (val: string) => {
changeUserAge(Number(val));
};
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input
type="text"
onChange={(e) => changeUserName(e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => ageInputChange(e.currentTarget.value)}
/>
</div>
</div>
);
}
结果如下
Recoil
使用 Recoil 会为你创建一个数据流向图,从 atom (共享状态)到 selector(纯函数),再流向 React 组件。Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。
安装依赖
javascript
npm install recoil
使用
RecoilRoot
要在组件中使用 Recoil,可以将 RecoilRoot
放置在父组件的某个位置。最好直接将其放在根组件。
tsx
import { createRoot } from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import router from "./route";
import { RecoilRoot } from "recoil";
const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
<RecoilRoot>
<RouterProvider router={router} />
</RecoilRoot>
);
Atom
Atom 是状态的单位。它们可更新也可订阅:当 atom 被更新,每个被订阅的组件都将使用新值进行重渲染。它们也可以在运行时创建。可以使用 atom 替代组件内部的 state。如果多个组件使用相同的 atom,则这些组件共享 atom 的状态。
Atom 是使用 atom
函数创建的
javascript
import { atom } from "recoil";
export const nameState = atom({
key: "nameState", // Atom 需要一个唯一的 key 值,请确保它们在全局上的唯一性
default: "",
});
export const ageState = atom({
key: "ageState", // Atom 需要一个唯一的 key 值,请确保它们在全局上的唯一性
default: 0,
});
Selector
selector
是一个纯函数,入参为 atom 或者其他 selector。当上游 atom 或 selector 更新时,将重新执行 selector 函数。组件可以像 atom 一样订阅 selector,当 selector 发生变化时,重新渲染相关组件。
tsx
import { atom, selector } from "recoil";
export const nameState = atom({
key: "nameState",
default: "",
});
export const ageState = atom({
key: "ageState",
default: 0,
});
export const introductionState = selector({
key: "introductionState",
get: ({ get }) => {
const name = get(nameState);
const age = get(ageState);
return `${name}今年${age}岁了`;
},
});
get
属性是用于计算的函数。它可以使用名为 get 的入参来访问 atom 以及其他 selector 的值。每当它访问另一个 atom 或 selector 时,就会创建相应的依赖关系,以便在更新另一个 atom 或 selector 时,该 atom 或 selector 也会被重新计算。
useRecoilState、useRecoilValue
要从组件中读取和写入 atom,我们需使用一个名为 useRecoilState
的 hook。和 React 的 useState
用法一致,但是这里的 state 可以在组件间共享使用。
可以使用 useRecoilValue()
读取 selector。它使用 atom 或 selector 作为参数,并返回相应的值。
tsx
// A组件
import { useRecoilState, useRecoilValue } from "recoil";
import { nameState, ageState, introductionState } from "@/store/user";
export default function AComponent() {
const [name] = useRecoilState(nameState);
const [age] = useRecoilState(ageState);
const introduction = useRecoilValue(introductionState);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{name}</div>
<div>用户年龄:{age}</div>
<div>{introduction}</div>
</div>
);
}
tsx
// B组件
import { useRecoilState, useRecoilValue } from "recoil";
import { nameState, ageState, introductionState } from "@/store/user";
export default function BComponent() {
const [name, setName] = useRecoilState(nameState);
const [age, setAge] = useRecoilState(ageState);
const changeUserName = (val: string) => {
setName(val);
};
const changeUserAge = (val: string) => {
setAge(Number(val));
};
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input
type="text"
onChange={(e) => changeUserName(e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => changeUserAge(e.currentTarget.value)}
/>
</div>
</div>
);
}
结果如下:
Jotai
Jotai
是一个原始而灵活的 React 状态管理库。 它采用自下而上的方法,采用受 Recoil
启发的原子模型。
安装依赖
javascript
npm install jotai
使用
atom
atom
功能是创建一个原子配置。
typescript
import { atom } from "jotai";
export const nameAtom = atom("");
export const ageAtom = atom(0);
还可以创建派生原子。 有三种模式:
- 只读 atom
- 只写 atom
- 读写 atom
typescript
import { atom } from "jotai";
export const nameAtom = atom("");
export const ageAtom = atom(0);
// 只读 atom
export const introductionAtom = atom(
(get) => `${get(nameAtom)}今年${get(ageAtom)}岁了`
);
// 只写 atom
export const changeNameAtom = atom(
null, // 为第一个参数传递 `null` 是一种惯例
(get, set, param: string) => {
set(nameAtom, param);
}
);
// 读写 atom
export const readWriteAgeAtom = atom(
(get) => get(ageAtom) + "岁",
(get, set, param: number) => {
set(ageAtom, param);
}
);
useAtom
useAtom
hook 用于读取状态中的原子值。useAtom
hook 以元组形式返回原子值和更新函数, 就像 React 的 useState
一样。 它需要一个使用 atom()
创建的原子配置。
除了useAtom
,还有常用的useAtomValue
、useSetAtom
-
useAtomValue
hook用于读取状态中的原子值,只能读取。 -
useSetAtom
hook则用于获取写入函数。
tsx
// A组件
import { useAtom, useAtomValue } from "jotai";
import { nameAtom, readWriteAgeAtom, introductionAtom } from "@/store/user";
export default function AComponent() {
const [name] = useAtom(nameAtom);
const [age] = useAtom(readWriteAgeAtom);
const introduction = useAtomValue(introductionAtom);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{name}</div>
<div>用户年龄:{age}</div>
<div>{introduction}</div>
</div>
);
}
tsx
// B组件
import { useAtom, useSetAtom } from "jotai";
import {
nameAtom,
ageAtom,
readWriteAgeAtom,
changeNameAtom,
} from "@/store/user";
export default function BComponent() {
const [, setName] = useAtom(nameAtom);
const [, setAge] = useAtom(ageAtom);
const setUserName = useSetAtom(changeNameAtom);
const setUserAge = useSetAtom(readWriteAgeAtom);
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input type="text" onChange={(e) => setName(e.currentTarget.value)} />
</div>
<div>
<span>修改用户姓名2:</span>
<input
type="text"
onChange={(e) => setUserName(e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => setAge(Number(e.currentTarget.value))}
/>
</div>
<div>
<span>修改用户年龄2:</span>
<input
type="number"
onChange={(e) => setUserAge(Number(e.currentTarget.value))}
/>
</div>
</div>
);
}
结果如下
用useSetAtom有时候可以起到性能优化的作用。
例如我们修改一下B组件做个演示
tsx
// B组件
import { useAtom, useSetAtom } from "jotai";
import { nameAtom, changeNameAtom } from "@/store/user";
export default function BComponent() {
const [, setName] = useAtom(nameAtom);
console.log("B组件渲染了");
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input type="text" onChange={(e) => setName(e.currentTarget.value)} />
</div>
</div>
);
}
可以发现B组件在修改状态时会触发重新渲染,但其实这里B组件只做修改功能,不需要重新渲染。
而改成useSetAtom后
tsx
// B组件
import { useAtom, useSetAtom } from "jotai";
import { nameAtom, changeNameAtom } from "@/store/user";
export default function BComponent() {
const setName = useSetAtom(changeNameAtom);
console.log("B组件渲染了");
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input type="text" onChange={(e) => setName(e.currentTarget.value)} />
</div>
</div>
);
}
可以发现换成useSetAtom后就不会触发B组件重新渲染了。
valtio
Valtio使React的代理状态变得简单。
安装依赖
tsx
npm i valtio
基本Api
valtio的基本api有proxy
、useSnapshot
- proxy: 代理跟踪原始对象和所有嵌套对象的变化
- useSnapshot: 创建捕捉变化的本地快照。
使用
typescript
import { proxy } from "valtio";
export const user = proxy({
name: "",
age: 0,
});
tsx
// A组件
import { useSnapshot } from "valtio";
import { user } from "@/store/user";
export default function AComponent() {
const userSnap = useSnapshot(user);
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{userSnap.name}</div>
<div>用户年龄:{userSnap.age}</div>
</div>
);
}
tsx
// B组件
import { user } from "@/store/user";
export default function BComponent() {
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input
type="text"
onChange={(e) => (user.name = e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => (user.age = Number(e.currentTarget.value))}
/>
</div>
</div>
);
}
结果如下
hox
与redux、zustand、recoil 这些状态管理库不同的是,它们最本质的能力还是对数据的操作。而Hox 想解决的问题,不是如何组织和操作数据,不是数据流的分层、异步、细粒度,Hox作者希望 Hox 只聚焦于一个痛点:在多个组件间共享状态。
安装依赖
javascript
npm i hox
全局store
可以通过 createGlobalStore
来创建一个全局 store
typescript
import { useState } from "react";
import { createGlobalStore } from "hox";
export const [useUseStore, getUserStore] = createGlobalStore(() => {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
function changeName(name: string) {
setName(name);
}
function changeAge(age: number) {
setAge(age);
}
return {
name,
age,
changeName,
changeAge,
};
});
createGlobalStore
返回了一个数组,里面有两个元素,你可以通过 ES6 的数组解构语法把他们解构出来
第一个元素是用来订阅和消费 store 的 Hook 函数;
第二个元素是一次性地读取 store 当前最新的值,但不会触发持续的订阅,因为它不是 Hook
tsx
// A组件
import { useUseStore, getUserStore } from "@/store/user";
export default function AComponent() {
const { name, age } = useUseStore();
function getStore() {
const store = getUserStore();
console.log(store);
}
return (
<div>
<h3>A组件</h3>
<div>用户姓名:{name}</div>
<div>用户年龄:{age}</div>
<button onClick={getStore}>获取store数据</button>
</div>
);
}
tsx
// B组件
import { useUseStore } from "@/store/user";
export default function BComponent() {
const { changeName, changeAge } = useUseStore();
return (
<div>
<h3>B组件</h3>
<div>
<span>修改用户姓名:</span>
<input
type="text"
onChange={(e) => changeName(e.currentTarget.value)}
/>
</div>
<div>
<span>修改用户年龄:</span>
<input
type="number"
onChange={(e) => changeAge(Number(e.currentTarget.value))}
/>
</div>
</div>
);
}
store
除了全局store外,还可以用createStore
创建普通store
在 hox 中,任意的 custom Hook,经过 createStore
包装后,就变成了持久化,可以在组件间进行共享的状态。
tsx
import { useState } from "react";
import { createStore } from "hox";
const [useCountStore, CountStoreProvider] = createStore(() => {
const [count, setCount] = useState(0);
function addCount() {
setCount((v) => v + 1);
}
return {
count,
addCount,
};
});
// A组件
function AComponent() {
const { count } = useCountStore();
return (
<div className="component-wrap">
<h3>A组件</h3>
<div>
<span>数值:</span>
<div>{count}</div>
</div>
</div>
);
}
// B组件
function BComponent() {
const { addCount } = useCountStore();
return (
<div className="component-wrap">
<h3>B组件</h3>
<div>
<button onClick={addCount}>增加</button>
</div>
</div>
);
}
export default function CComponent() {
return (
<div>
<CountStoreProvider>
<AComponent></AComponent>
<BComponent></BComponent>
</CountStoreProvider>
</div>
);
}
createStore
会返回一个数组,里面有两个元素,你可以通过 ES6 的数组解构语法把他们解构出来
第一个元素是用来订阅和消费 store 的 Hook 函数(和createGlobalStore
返回的第一个元素一样);
第二个元素是是状态的容器,它的底层是依赖了 React Context 所以你需要把它注入到组件树中。
上面代码效果如下:
状态管理库对比
可以从 npm trends 看各个状态管理库近一年的下载量趋势
可以发现,Redux作为老牌状态管理库,地位依然稳固。但是近几年出现的jotai、zustand、valtio、recoil等新的状态管理库,由于它们更简单的操作,更小的体积,更好的性能等优点,热度也在逐步上升,特别是zustand,是其中最流行的一个。而hox则用的人还是比较少。
redux | zustand | jotai | recoil | valtio | hox | |
---|---|---|---|---|---|---|
github Stars | 60.6k | 44.6k | 17.8k | 19.5k | 8.7k | 1.4k |
created | 2011 | 2019 | 2020 | 2020 | 2020 | 2019 |