zustand是最近比较出圈的一款小而美的状态管理库,基于发布-订阅的模式,只需通过一个create函数创建一个store来存储state和action,然后在组件内就可以引入这个store hook来获取状态,而不需要像redux一样用connect将状态和组件联系起来,也不需要将组件用context provider包裹,大大降低了复杂性,而且zustand还导出了一些中间件供使用(比如持久化函数persist),对于一般的web项目完全够用。
基本用法(React)
安装
yarn add zustand
创建一个Store
store 是一个 hook,你可以在里面放任何东西:基本类型值、对象、函数。而set
函数会合并状态。
jsx
import create from "zustand";
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
然后绑定组件
可以在任意一个组件内引入这个store hook,组件会在你选择的状态变化时重新渲染。
jsx
function BearCounter() {
const bears = useStore((state) => state.bears); // 获取state
return <h1>{bears} around here ...</h1>;
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation); // 获取action
return <button onClick={increasePopulation}>one up</button>;
}
就这么简单!!!
当然zustand也支持选择器selector和异步action。
获取所有状态
jsx
const state = useStore(); // 这将导致任何一个状态变更都会引起组件重新渲染
获取特定状态
jsx
const nuts = useStore((state) => state.nuts);
const honey = useStore((state) => state.honey);
默认是基于严格相等来判断的前后状态是否一致
异步action
jsx
const useStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond);
set({ fishies: await response.json() });
},
}));
在action里读取状态
set
允许函数式更新:set(state => result)
,但你仍然可以通过get
访问状态。
jsx
const useStore = create((set, get) => ({
sound: "grunt",
action: () => {
const sound = get().sound
// ...
}
})
如何在zustand中管理请求数据
既然zustand的action支持异步,那自然想到可以用异步action来请求后端数据,再通过set
方法改变state。
js
// store
import {create} from 'zustand';
import axios from 'axios';
export const useStore = create((set, get) => ({
products: [],
getProducts: async (key) => {
console.log('==========request=========')
const {data} = await axios.get(key);
set(({products: data.products}));
}
}));
然后在组件内使用
jsx
import { Suspense, useEffect } from "react";
import { Spin } from "antd";
import { useStore } from "@/store";
import { API_PRODUCTS } from "@/APIs";
const Products = () => {
return (
<div>
<Suspense fallback={<Spin spinning size="large"/>}>
<ProductList />
</Suspense>
</div>
);
};
function ProductList() {
const {getProducts, products} = useStore(); // 获取状态
useEffect(() => {
console.log('==========rendering=======')
getProducts(API_PRODUCTS.getProducts); // 调用接口
}, [])
return (
<>
<table>
<thead>
<tr>
<td>
<th>Product Name</th>
</td>
<td>
<th>Price</th>
</td>
<td>
<th>Brand</th>
</td>
<td>
<th>Operation</th>
</td>
</tr>
</thead>
<tbody>
{products.map((product)=> (
<tr key={product.id}>
<td>{product.title}</td>
<td>{"$" + product.price}</td>
<td>{product.brand}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
运行效果如下:
可以在控制台看到,首次渲染的时候会渲染两次,而且还会请求两次接口,重network tab也可以看到
分析原因可知,在创建store的方法里有个异步actiongetProducts
,该方法调用接口返回data,然后通过set
方法修改状态products
。
而在ProductList
组件中,通过store暴露出的getProducts
方法,在副作用里调用后,此时异步action已经获取到了products
,useStore
里拿到products
渲染页面。
这里渲染、请求两次的问题就在于,set
方法会导致组件重新渲染。
第一次render是在组件useEffect
里发送请求,然后set
方法更改状态,这时触发组件再次渲染,组件再次发送请求,所以才导致了两次render和两次request。
还有一处不是很优雅的地方在于,虽然store里能直接发送异步请求,但是要暴露一个getProducts
方法给到组件,而这个暴露出的方法又只能在副作用里执行,那为何不能直接在组件内请求数据然后通过useStore.setState()
改变状态呢?这样也可以达到store管理请求数据的效果。只不过这样又回到了最开始学习React的时候,总是喜欢把副作用放在useEffect
内执行,这样做当然没问题,不过现在用Hooks了,一切以useHooks
作为出发点去思考:
如果把数据请求的方法用Hooks实现呢?
如果在组件首次渲染时就能拿到数据呢?
如何保证store既更改了状态又不重新发送请求呢?
在Store里引入SWR
SWR应该不用过多介绍了吧,一个用于数据请求的 React Hooks 库。
改造store
js
import axios from "axios";
import useSWR from 'swr';
import {create} from 'zustand';
export const getFetcher = (url) => {
return axios.get(url).then((res) => res.data);
};
export const useStore = create((set, get) => ({
products: [],
getProducts: (key) => {
console.log('==========request=========');
return useSWR(key, getFetcher, {
suspense: true, // 开启React Suspense
onSuccess: (data) => set(() => ({products: data.products})) // 在回调函数里处理状态变更
})
},
}));
组件里使用
jsx
import { Suspense } from "react";
import { Spin } from "antd";
import { API_PRODUCTS } from "@/APIs";
import { useStore } from "@/store";
import { API_PRODUCTS } from "@/APIs";
const Products = () => {
return (
<div>
<Suspense fallback={<Spin spinning size="large"/>}>
<ProductList />
</Suspense>
</div>
);
};
function ProductList() {
const {data} = useStore(s => s.getProducts(API_PRODUCTS.getProducts)); // 传入selector,获取特定的action
console.log('=========render=========')
return (
<>
<table>
<thead>
<tr>
<td>
<th>Product Name</th>
</td>
<td>
<th>Price</th>
</td>
<td>
<th>Brand</th>
</td>
<td>
<th>Operation</th>
</td>
</tr>
</thead>
<tbody>
{data.products.map((product) => (
<tr key={product.id}>
<td>{product.title}</td>
<td>{"$" + product.price}</td>
<td>{product.brand}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
从运行截图可以看到,组件渲染两次,但数据只请求了一次。
总结
基于React Hook的SWR可以在函数组件的顶层就拿到数据,减少了组件副作用的侵入,并且自带的缓存功能减少不必要的请求。