在React里用zustand管理请求接口返回的数据

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已经获取到了productsuseStore里拿到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可以在函数组件的顶层就拿到数据,减少了组件副作用的侵入,并且自带的缓存功能减少不必要的请求。

相关推荐
叁分之一40 分钟前
“我打包又失败了”
前端·npm
tang游戏王12340 分钟前
AJAX进阶-day4
前端·javascript·ajax
无语听梧桐1 小时前
vue3中使用Antv G6渲染树形结构并支持节点增删改
前端·vue.js·antv g6
Ratten1 小时前
【taro react】(游戏) ---- 贪吃蛇
react.js
go2coding1 小时前
开源 复刻GPT-4o - Moshi;自动定位和解决软件开发中的问题;ComfyUI中使用MimicMotion;自动生成React前端代码
前端·react.js·前端框架
freesharer1 小时前
Zabbix 配置WEB监控
前端·数据库·zabbix
web前端神器1 小时前
forever启动后端服务,自带日志如何查看与设置
前端·javascript·vue.js
是Yu欸1 小时前
【前端实现】在父组件中调用公共子组件:注意事项&逻辑示例 + 将后端数组数据格式转换为前端对象数组形式 + 增加和删除行
前端·vue.js·笔记·ui·vue
今天是 几 号2 小时前
WEB攻防-XSS跨站&反射型&存储型&DOM型&标签闭合&输入输出&JS代码解析
前端·javascript·xss
A-超2 小时前
html5 video去除边框
前端·html·html5