《Zustand 极简状态管理:快速入门班》

407 阅读4分钟

image.png

拥抱 Zustand 的优雅——你的 React 状态管理就该这么丝滑!

前言

React的状态管理库有很多种,大致可以分为以下几种:

  • React内置状态管理库,useState/useReducer、useContext
  • 全局状态管理库,Redux、MobX、Zustand
  • 原子化状态管理库,Recoil、Jotai

作为后起之秀的Zustand,以轻量简洁、高性能、支持TS深受广大开发者的喜爱。

image.png

周下载量更是高达六百多万。

Zustand的使用

安装

npm install zustand

快速上手

声明状态

import { create } from "zustand";

type Action = {
  toggleTheme: () => void;
};

type State = { theme: string}

const themeStore = create<Action & State>((set) => ({
  theme: "light",
  toggleTheme: () => {
    return set((state) => ({
        theme: state.theme === "light" ? "dark" : "light",
      }))
  },
}));

export default themeStore;

使用

import { useEffect } from 'react'
import themeStore from './store/theme'

function ThemeChange() {
  const theme = useThemeStore((state) => state.theme)
  const toggleTheme = useThemeStore((state) => state.toggleTheme)
  return <button onClick={toggleTheme}>toggle theme: {theme}</button>
}

就这样我们简单创建了一个主题切换的转态管理库,是不是 so easy。

以上我们只是创建了一个单纯的主题状态切换,切换主题后,我们应该如何监听主题的变化去修改样式嘞?那就需要用到subscribe了。

useEffect(() => {
  const unsub = useThemeStore.subscribe((state) => {
    document.body.classList.remove('light', 'dark');
    document.body.classList.add(state.theme);
  });
  // 初始化
  document.body.classList.remove('light', 'dark');
  document.body.classList.add(useThemeStore.getState().theme);
  return () => unsub();
}, []);

Zustand中间件

Persist数据持久化

有一些数据可能需要存在浏览器内存中,以便你在页面重新加载或应用程序重新启动时持久化存储的状态,比如说上个例子中的主题状态就需要持久化,以便用户能拥有更好的体验效果。

import { create } from "zustand";
import { persist } from "zustand/middleware";

type State = { theme: string };
type Action = { toggleTheme: () => void };

const useThemeStore = create<State & Action>()(
  persist(
    (set) => ({
      theme: "light",
      toggleTheme: () => {
        set((state) => ({
          theme: state.theme === "light" ? "dark" : "light",
        }));
      },
    }),
    { name: "themeStore" }
  )
);

export default useThemeStore;

Devtools调试

全局状态一旦多了我们就不好把控,需要借助调试工具去调试状态的变化,借助Redux DevTools我们可以去调试Zustand的状态变化。

import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

type State = { theme: string };
type Action = { toggleTheme: () => void };

const useThemeStore = create<State & Action>()(
  devtools(
    persist(
      (set) => ({
        theme: "light",
        toggleTheme: () => {
          set((state) => ({
            theme: state.theme === "light" ? "dark" : "light",
          }));
        },
      }),
      { name: "themeStore" }
    )
  )
);

export default useThemeStore;

Immer执行不可变

在React中对于一些对象的更改,我们可以ES6的解构语法{...XXX},将其解构出来出来后重新赋值,而immer中间件让您执行不可变的更新,操作更加方便。

在开始的时候我们需要安装immer库

import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

type State = {
  person: {
    name: string;
    age: number;
    email: string;
    address: {
      street: string;
      city: string;
      state: string;
    };
  };
};

type Action = {
  setName: (name: string) => void;
  setCity: (city: string) => void;
};

// ES6解构的方法
// const usePersonStore = create<State & Action>()(
//   devtools((set) => ({
//     person: {
//       name: "John",
//       age: 30,
//       email: "<EMAIL>",
//       address: {
//         street: "123 Main St",
//         city: "New York",
//         state: "NY",
//       },
//     },
//     setName: (name) =>
//       set((state) => ({ ...state, person: { ...state.person, name } })),
//     setCity: (city) =>
//       set((state) => ({
//         ...state,
//         person: { ...state.person, address: { ...state.person.address, city } },
//       })),
//   }))
// );

const usePersonStore = create<State & Action>()(
  devtools(
    immer((set) => ({
      person: {
        name: "John",
        age: 30,
        email: "<EMAIL>",
        address: {
          street: "123 Main St",
          city: "New York",
          state: "NY",
        },
      },
      setName: (name) =>
        set((state) => {
          state.person.name = name;
        }),
      setCity: (city) =>
        set((state) => {
          state.person.address.city = city;
        }),
    }))
  )
);

export default usePersonStore;

Combine自动推断类型

以往我们需要像上面那样显示定义类型,现在我们可以通过combine去隐式定义,自动帮我们推断出类型,又加快了开发速度。不得不说,zustand项目的ts运用得真6。

import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

const usePersonStore = create(
  devtools(
    immer(
      combine(
        {
          person: {
            name: "John",
            age: 30,
            email: "<EMAIL>",
            address: {
              street: "123 Main St",
              city: "New York",
              state: "NY",
            },
          },
        },
        (set) => ({
          setName: (name: string) =>
            set((state) => {
              state.person.name = name;
            }),
          setCity: (city: string) =>
            set((state) => {
              state.person.address.city = city;
            }),
        })
      )
    )
  )
);

export default usePersonStore;

SubscribeWithSelector精准订阅

Store存在多个状态,在订阅的时候只要一触发事件,都是重新订阅,降低了性能。

import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";

const useThemeStore = create(
  devtools(
    combine(
      {
        theme: "dark",
        lang: "zh"
      },
      (set) => ({
        toggleTheme: () => {
          set((state) => ({
            theme: state.theme === "light" ? "dark" : "light",
          }));
        },

        toggleLang: () => {
          set((state) => ({
            lang: state.lang === "zh" ? "en" : "zh",
          }));
        },
      })
    )
  )
);

export default useThemeStore;
  useEffect(() => {
    // 监听 theme 变化并切换 body class
    const unsub = useThemeStore.subscribe((state) => {
      console.log('触发事件', state.theme, state.lang);

      document.body.classList.remove('light', 'dark');
      document.body.classList.add(state.theme);
    });
    // 初始化
    document.body.classList.remove('light', 'dark');
    document.body.classList.add(useThemeStore.getState().theme);
    return () => unsub();
  }, []);

在上面这个代码中两个方法,toggleTheme、toggleLang的触发都会订阅,打印触发事件,而这是只需要监听主题的变化,我们可以用subscribeWithSelector进行改造。

import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";
import { subscribeWithSelector } from "zustand/middleware";

const useThemeStore = create(
  subscribeWithSelector(
    devtools(
      combine(
        {
          theme: "dark",
          lang: "zh"
        },
        (set) => ({
          toggleTheme: () =>
            set((state) => ({
              theme: state.theme === "dark" ? "light" : "dark",
            })),
          toggleLang: () =>
            set((state) => ({
              lang: state.lang === "zh" ? "en" : "zh",
            })),
        })
      )
    )
  )
);

export default useThemeStore;
  useEffect(() => {
    // 监听 theme 变化并切换 body class
    const unsub = useThemeStore.subscribe((state) => state.theme, (theme) => {
      console.log('触发事件', theme);
      document.body.classList.remove('light', 'dark');
      document.body.classList.add(theme);
    });
    // 初始化
    document.body.classList.remove('light', 'dark');
    document.body.classList.add(useThemeStore.getState().theme);
    return () => unsub();
  }, []);

在触发toggleLang事件就不会打印触发事件

Redux模仿reducer操作

习惯了redux的操作习惯,切换zustand的操作还不习惯,我们可以使用redux中间件,使得继续像reducer一样操作状态的切换。

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

// 定义 state 和 action 类型
type CounterState = {
  count: number
}

type CounterActions = {
  dispatch: (action: { type: string; payload?: any }) => void
}

// reducer 函数
function counterReducer(state: CounterState, action: { type: string; payload?: any }): CounterState {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 }
    case 'DECREMENT':
      return { count: state.count - 1 }
    case 'ADD':
      return { count: state.count + (action.payload ?? 0) }
    default:
      return state
  }
}

// 创建 store,暴露 dispatch
const useCounterReduxLike = create<CounterState & CounterActions>()(
  devtools((set, get) => ({
    count: 0,
    dispatch: (action) => set((state) => counterReducer(state, action)),
  }))
)

export default useCounterReduxLike;
import useCounterReduxLike from './store/counterReduxLike';

function Counter() {
  const count = useCounterReduxLike((s) => s.count)
  const dispatch = useCounterReduxLike((s) => s.dispatch)
  return (
    <div>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'ADD', payload: 5 })}>+5</button>
    </div>
  )
}

总结

Zustand作为新生的react全局状态管理库,简单明了,容易上手,其中中间件更是赋予了新的活力。

我们学了Persist 持久化、Devtools 调试、Immer 不可变...... 学完这篇,现在你的 React 状态管理直接起飞 🚀。