首页 > 其他分享 >用Zustand实现组件级状态管理的最佳实践

用Zustand实现组件级状态管理的最佳实践

时间:2024-08-26 09:18:07浏览次数:4  
标签:状态 Zustand set const bears React 最佳 组件

在前文中,我们介绍了Zustand这个简单、易用、轻量的状态管理框架。

通常情况下,状态管理通常都是全局的,可以在应用的任意地方访问。然而,这样的做法是否真的符合最佳实践呢?如果从马克思的角度来看,任何片面的观点都是不全面的。事实上,有些时候我们只想创建页面级别或者组件级别的状态,而不是把所有状态都挂在全局。

全局状态的弊端

无效渲染

全局状态管理的一个明显弊端是它可能导致无效的渲染。全局状态通常是在React组件生命周期之外创建的,这意味着我们无法利用组件的props值来设置初始状态。我们只能通过一个默认值来创建状态,然后再利用useEffectprops中的值同步到store中:

const useBearStore = create((set) => ({
  // 提供一个初始值
  bears: 0,
  actions: {
    increasePopulation: (by) =>
      set((state) => ({ bears: state.bears + by })),
    removeAllBears: () => set({ bears: 0 }),
  },
}));

const App = ({ initialBears }) => {
  // 写入一个真实的初始值
  React.useEffect(() => {
    useBearStore.set((prev) => ({ ...prev, bears: initialBears }));
  }, [initialBears]);

  return (
    <main>
      <RestOfTheApp />
    </main>
  );
};

在上面的例子中,组件在useEffect触发之前会使用初始的bears: 0渲染一次,然后在正确的initialBears赋值后再次渲染。我们只是在同步initialBears值,而不是用它来初始化状态,但这依然会导致多次渲染。

难以管理

全局状态的另一个弊端是难以管理。在应用的任意部分,全局状态都可能被意外访问或修改,这使得在项目后续迭代中难以保证状态的安全隔离,甚至可能导致状态混乱。

假设你有一个应用,包含用户信息和购物车信息的全局状态。初始设计时,这两个状态是分离的,但在某次迭代中,开发者意外地修改了购物车状态中的用户信息,从而导致状态混乱。

import create from 'zustand';

// 用户信息状态
const useUserStore = create(set => ({
  user: { name: 'John Doe', loggedIn: true },
  updateUser: (newUser) => set({ user: newUser }),
}));

// 购物车信息状态
const useCartStore = create(set => ({
  cart: [],
  addItem: (item) => set(state => ({ cart: [...state.cart, item] })),
}));

在这个初始设计中,用户信息和购物车信息是分开的,全局状态管理看起来较为清晰。

但在项目的后续迭代中,假如开发者需要在购物车中添加一些用户相关的信息,错误地修改了useCartStore,直接访问和修改了用户信息:

const useCartStore = create(set => ({
  cart: [],
  user: { name: 'John Doe', loggedIn: true }, // 这里重复了用户状态
  addItem: (item) => set(state => ({ cart: [...state.cart, item] })),
  updateUserInCart: (newUser) => set({ user: newUser }), // 修改了购物车中的用户状态
}));

这个设计有两个主要问题:

  • 问题 1: 用户状态现在同时存在于useUserStoreuseCartStore中,导致了状态的重复和混乱。
  • 问题 2: 其他开发者可能在后续开发中未意识到状态已经被重复定义,并可能会意外地通过useCartStore修改user,导致状态的不一致和难以追踪的bug。

虽然这个例子看起来容易发现问题,但在更复杂的真实场景中,类似问题可能会更加隐蔽。如果一个框架无法提供安全的实践方案,人为的错误在所难免,尤其是在大型项目中。因此,我们需要在架构设计时提供良好的规范,以减少错误的发生。

如何处理

因此,在一个应用中,状态应该被分为全局状态和局部状态。那么,Zustand如何实现局部状态呢?

我们可以通过React Context来注入局部状态。这个概念类似于React Query中的<QueryClientProvider>,以及Redux中的单一状态仓库。因为状态仓库的实例是静态的、单例的,不会频繁改变,所以将它们放到React Context中非常容易,并且不会导致不必要的重新渲染。然后,我们仍然可以为状态仓库创建订阅者,这些订阅者将通过Zustand进行优化。以下是具体的实现:

import { createStore, useStore } from 'zustand';

const BearStoreContext = React.createContext(null);

const BearStoreProvider = ({ children, initialBears }) => {
  const [store] = React.useState(() =>
    createStore((set) => ({
      bears: initialBears,
      actions: {
        increasePopulation: (by) =>
          set((state) => ({ bears: state.bears + by })),
        removeAllBears: () => set({ bears: 0 }),
      },
    }))
  );

  return (
    <BearStoreContext.Provider value={store}>
      {children}
    </BearStoreContext.Provider>
  );
};

在这里,我们没有使用开箱即用的create函数来创建实例,因为它返回的是一个React hook(useStore),而通过createStore可以返回一个独立的store对象,这是Zustand的新API。

我们使用了React.useState来创建store,虽然也可以使用React.useRef,但前者对TypeScript更加友好。使用useState的初始化方法只会调用一次,因此props的更新将不会传递到状态仓库中。

如果我们想要从状态仓库中取出一些值进行消费,可以使用这个上下文。为此,我们需要将store和selector传递给从Zustand中拿到的useStore钩子。以下是一个最佳实践的抽象:

const useBearStore = (selector) => {
  const store = React.useContext(BearStoreContext);
  if (!store) {
    throw new Error('Missing BearStoreProvider');
  }
  return useStore(store, selector);
};

相较于创建一个全局状态而言,这种方式虽然多了一些代码,但它解决了三个关键问题:

  1. 可以利用props初始化状态仓库:因为我们是在React组件树内部创建的store。
  2. 自动化测试更为容易:我们可以选择渲染一个包含BearStoreProvider的组件,或者为测试场景渲染一个独立的组件。这样,已创建的状态仓库能够完全隔离测试,无需在测试间重置状态仓库。
  3. 组件复用性增强:现在,一个组件可以渲染一个BearStoreProvider,为其子组件提供封装好的Zustand状态仓库。我们可以在一个页面中任意渲染这个组件,每个实例将拥有它独立的状态仓库,从而实现状态的隔离和复用。

即便Zustand文档中声称无需Context Provider也能访问状态仓库,但了解如何整合状态仓库的创建和React Context仍然是必要的,这样可以更好地处理一些需要封装和复用的场景。

本文由mdnice多平台发布

标签:状态,Zustand,set,const,bears,React,最佳,组件
From: https://www.cnblogs.com/miniwa/p/18380004

相关文章

  • 在 .NET 8 中搜索值的最佳新方法
    https://www.bilibili.com/list/watchlater?oid=918750121usingSystem.Buffers;usingBenchmarkDotNet.Attributes;namespaceSearchValuesExample;[MemoryDiagnoser(false)]publicclassBenchmarks{privateconststringBase64Chars="ABCDEFGHIJKLMNO......
  • 19 OptionMenu 组件
    OptionMenu组件使用指南Tkinter的OptionMenu组件是一个下拉选择框,允许用户从一组预定义的选项中选择一个。它通常用于提供用户一个有限的选项集合来选择。以下是对OptionMenu组件的详细说明和一个使用案例。OptionMenu组件属性variable:与OptionMenu组件关联的......
  • 在C#中应用命令模式:设计和实现的最佳实践
    在C#中应用命令模式:设计和实现的最佳实践引言在软件设计中,设计模式是解决常见问题的通用解决方案。命令模式(CommandPattern)是行为型设计模式之一,它将请求或操作封装为对象,从而使得你可以用不同的请求对客户端进行参数化,队列请求或记录请求日志,以及支持可撤销的操作。在C#......
  • Axure优质数据可视化大屏模板+图表组件+科技感元件
    Axure优质数据可视化大屏模板+图表组件+科技感元件Axure精心构建的数据可视化解决方案,震撼发布!我们汇集了110套顶尖大屏可视化模板,覆盖从基础监控到复杂分析的全场景需求,每套模板均经过精心设计,旨在为您的数据展示增添无限可能。此外,还配备了超过200种图表组件,包括交互式......
  • vue3的天气组件vue3-mini-weather为何安装会报错?
    参考于:https://gitee.com/maocaoying_admin/vue3-mini-weather安装上述地址的组件报错:实现的效果图:实现步骤:1将vue3-mini-weather的lib直接拿到自己的项目中来:2将lib中的组件引入到自己项目中使用点击查看代码<template><divclass="section-income"><div......
  • 【Material-UI】深入探讨Radio Group组件的自定义功能
    文章目录一、RadioGroup组件概述1.组件介绍2.自定义的重要性二、RadioGroup组件的自定义1.样式定制示例2.代码详解3.样式自定义的注意事项三、如何利用自定义功能提升用户体验1.提升视觉一致性2.增强可用性3.实现更灵活的布局四、总结Material-UI是R......
  • C++:STL六大组件,知识点总结。
    STL知识点总结STL是C++标准库中的一个重要部分,提供了一组灵活通用的数据结构,核心是模板类。接下来是STL的主要组件及其功能简介。1.容器容器是用来存储和管理一组数据的对象。不同的容器适用于不同类型的数据存储需求。可理解为各种形式实现的存储结构顺序容器vec......
  • 【微信小程序开发】栀子手作花花微信小程序商城开发最佳实践
    本文介绍了通过uniapp技术实现了一套栀子手作在线购物商城系统。包含首页、分类、我的等常用功能入口。一、功能演示首页:包含了商品介绍,领劵中心和商品列表区域。商品分类:支持不同的商品分类和商品搜素。商品详情:包含了商品详细的描述信息,透出了分享、首页、客服等入口。......
  • 052、Vue3+TypeScript基础,页面通讯之一个组件中多个v-model数据绑定
    01、main.js代码如下://引入createApp用于创建Vue实例import{createApp}from'vue'//引入App.vue根组件importAppfrom'./App.vue'//引入emitter用于全局事件总线//importemitterfrom'@/utils/emitter'constapp=createApp(App);//App.vue的根元素id为......
  • Superset BI封装自定义组件(堆叠柱状图)
    目录前言封装步骤一、创建组件文件夹二、预设组件信息三、使用组件往期回顾前言Superset是一个现代化的、易于使用的、轻量级的数据可视化工具,它允许用户通过简单的点击操作来创建和分享图表。如果你想在Superset中创建自定义组件,你可能需要进行一些扩展工作。......