Skip to content

React 状态管理

基本使用

ts
import { create } from "zustand";

interface ICntStore {
  cnt: number;
  incCnt: () => void;
  decCnt: () => void;
  resetCnt: () => void;
  getCnt: () => number;
}

const useCntStore = create<ICntStore>((set, get) => {
  return {
    cnt: 0,
    incCnt: () => set((state) => ({ cnt: state.cnt + 1 })),
    decCnt: () => set((state) => ({ cnt: state.cnt - 1 })),
    resetCnt: () => set({ cnt: 0 }),
    getCnt: () => get().cnt,
  };
});

export default useCntStore;
tsx
import useCntStore from "@/stores/cnt";

function Left() {
  const { incCnt, decCnt, resetCnt } = useCntStore();
  return (
    <>
      <button onClick={incCnt}>+</button>
      <button onClick={decCnt}>-</button>
      <button onClick={resetCnt}>reset</button>
    </>
  );
}

function Right() {
  const incCnt = useCntStore((state) => state.incCnt);
  const decCnt = useCntStore((state) => state.decCnt);
  const resetCnt = useCntStore((state) => state.resetCnt);
  return (
    <>
      <button onClick={incCnt}>+</button>
      <button onClick={decCnt}>-</button>
      <button onClick={resetCnt}>reset</button>
    </>
  );
}

function App() {
  const cntStore = useCntStore();
  const { cnt } = cntStore;
  const getCnt = useCntStore((state) => state.getCnt);
  return (
    <>
      <Left />
      cnt: {cnt} {getCnt()}
      <Right />
    </>
  );
}

export default App;

深层次状态

immer

ts
import { produce } from "immer";

const data = { user: { name: "whoami", age: 22 } };
const newData = produce(data, (draft) => {
  draft.user.age = 23;
});

// {user: {name: 'whoami', age: 23}}, false
console.log(newData, newData === data);

zustand 使用 immer 中间件

ts
import { create } from "zustand";

interface IGroup {
  cnt: {
    female: number;
    male: number;
  };
  addMale: () => void;
}

const useGroupStore = create<IGroup>((set) => ({
  cnt: {
    female: 0,
    male: 1,
  },
  addMale: () =>
    set((state) => ({
      cnt: {
        ...state.cnt,
        male: state.cnt.male + 1,
      },
    })),
}));

export default useGroupStore;
ts
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

interface IGroup {
  cnt: {
    female: number;
    male: number;
  };
  addMale: () => void;
}

const useGroupStore = create<IGroup>()(
  immer((set) => ({
    cnt: {
      female: 0,
      male: 1,
    },
    addMale: () =>
      set((state) => {
        state.cnt.male += 1;
      }),
  })),
);

export default useGroupStore;

immer 原理: Proxy 代理

js
function produce(base, fn) {
  const clone = {};
  const handler = {
    get(target, prop, receiver) {
      if (prop in clone) {
        return clone[prop];
      }

      if (typeof target[prop] === "object" && target[prop] !== null) {
        return new Proxy(target[prop], handler);
      }

      return Reflect.get(target, prop, receiver);
    },

    set(target, prop, newValue) {
      return Reflect.set(clone, prop, newValue);
    },
  };

  const baseProxy = new Proxy(base, handler);
  fn(baseProxy);

  if (Object.keys(clone).length === 0) {
    return base;
  }

  return JSON.parse(JSON.stringify(baseProxy));
}

const obj = {
  user: {
    name: "whoami",
    age: 22,
  },
};

const newObj = produce(obj, (draft) => {
  draft.user.name = "immer";
  draft.user.age = 23;
});

console.log(newObj, newObj === obj);

状态切片, useShallow

  • 使用解构: setSing 时, 会导致 <First /> 组件更新; setDance 时, 也会导致 <First /> 组件更新
  • 使用状态切片: setSing 时, 会导致 <Second /> 组件更新; setDance 时, 不会导致 <Second /> 组件更新
  • 使用 useShallow 中间件: setSing 时, 会导致 <Third /> 组件更新; setDance 时, 不会导致 <Third /> 组件更新
ts
import { create } from "zustand";

interface IKun {
  name: string;
  hobbies: {
    sing: string;
    dance: string;
  };
  setSing: (newSing: string) => void;
  setDance: (newDance: string) => void;
}

const useKunStore = create<IKun>((set) => ({
  name: "kun",

  hobbies: {
    sing: "sing",
    dance: "dance",
  },

  setSing: (newSing: string) => {
    set((state) => ({
      ...state,
      hobbies: { ...state.hobbies, sing: newSing },
    }));
  },

  setDance: (newDance: string) => {
    set((state) => ({
      ...state,
      hobbies: { ...state.hobbies, dance: newDance },
    }));
  },
}));

export default useKunStore;
tsx
import useKunStore from "./stores/kun";
import { useShallow } from "zustand/react/shallow";

function Update() {
  const { setSing, setDance } = useKunStore();
  return (
    <div className="flex gap-5">
      <button onClick={() => setSing(sing + "!")}>setSing</button>
      <button onClick={() => setDance(dance + "!")}>setDance</button>
    </div>
  );
}

function First() {
  console.log("First update...");
  // 使用解构
  // setSing 时, 会导致 <First /> 组件更新
  // setDance 时, 也会导致 <First /> 组件更新
  const {
    name,
    hobbies: { sing },
  } = useKunStore();
  return (
    <div className="bg-slate-300">
      <div>First name: {name}</div>
      <div>First sing: {sing}</div>
    </div>
  );
}

function Second() {
  console.log("Second update...");
  // 使用状态切片
  // setSing 时, 会导致 <Second /> 组件更新
  // setDance 时, 不会导致 <Second /> 组件更新
  const name = useKunStore((state) => state.name);
  const sing = useKunStore((state) => state.hobbies.sing);
  return (
    <>
      <div>Second name: {name}</div>
      <div>Second sing: {sing}</div>
    </>
  );
}

function Third() {
  console.log("Third update...");
  // 使用 useShallow 中间件
  // setSing 时, 会导致 <Third /> 组件更新
  // setDance 时, 不会导致 <Third /> 组件更新
  const { name, sing } = useKunStore(
    useShallow((state) => ({
      name: state.name,
      sing: state.hobbies.sing,
    })),
  );
  return (
    <div className="bg-slate-300">
      <div>Third name: {name}</div>
      <div>Third sing: {sing}</div>
    </div>
  );
}

export default function App() {
  console.log("App update...");
  return (
    <>
      <Update />
      <First />
      <Second />
      <Third />
    </>
  );
}

中间件

  • immer 中间件
  • devtools 调试中间件
  • persist 持久化中间件
ts
import { create } from "zustand";

interface IKun {
  name: string;
  hobbies: {
    sing: string;
    dance: string;
  };
  setSing: (newSing: string) => void;
  setDance: (newDance: string) => void;
}

const useKunStore = create<IKun>((set) => ({
  name: "kun",

  hobbies: {
    sing: "sing",
    dance: "dance",
  },

  setSing: (newSing: string) => {
    set((state) => ({
      ...state,
      hobbies: { ...state.hobbies, sing: newSing },
    }));
  },

  setDance: (newDance: string) => {
    set((state) => ({
      ...state,
      hobbies: { ...state.hobbies, dance: newDance },
    }));
  },
}));

export default useKunStore;
tsx
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

interface IKun {
  name: string;
  hobbies: {
    sing: string;
    dance: string;
  };
  setSing: (newSing: string) => void;
  setDance: (newDance: string) => void;
}

const useKunStore = create<IKun>()(
  immer((set) => ({
    name: "kun",

    hobbies: {
      sing: "sing",
      dance: "dance",
    },

    setSing: (newSing: string) => {
      set((state) => {
        state.hobbies.sing = newSing;
      });
    },

    setDance: (newDance: string) => {
      set((state) => {
        state.hobbies.dance = newDance;
      });
    },
  })),
);

export default useKunStore;
ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

interface IKun {
  name: string;
  hobbies: {
    sing: string;
    dance: string;
  };
  setSing: (newSing: string) => void;
  setDance: (newDance: string) => void;
}

const useKunStore = create<IKun>()(
  immer(
    devtools(
      (set) => ({
        name: "kun",

        hobbies: {
          sing: "sing",
          dance: "dance",
        },

        setSing: (newSing: string) => {
          set((state) => {
            state.hobbies.sing = newSing;
          });
        },

        setDance: (newDance: string) => {
          set((state) => {
            state.hobbies.dance = newDance;
          });
        },
      }),
      {
        name: "kun",
        enabled: true,
      }, // optional
    ),
  ),
);

export default useKunStore;
ts
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

interface IKun {
  name: string;
  hobbies: {
    sing: string;
    dance: string;
  };
  setSing: (newSing: string) => void;
  setDance: (newDance: string) => void;
}

const useKunStore = create<IKun>()(
  immer(
    persist(
      (set) => ({
        name: "kun",

        hobbies: {
          sing: "sing",
          dance: "dance",
        },

        setSing: (newSing: string) => {
          set((state) => {
            state.hobbies.sing = newSing;
          });
        },

        setDance: (newDance: string) => {
          set((state) => {
            state.hobbies.dance = newDance;
          });
        },
      }),
      {
        // localStorage, sessionStorage, ... 的 key
        name: "kun",
        storage: createJSONStorage(() => localStorage),
        // 持久化部分状态
        partialize: (state) => ({
          name: state.name,
          sing: state.hobbies.sing,
        }),
      },
    ),
  ),
);

export default useKunStore;
tsx
import { useShallow } from "zustand/shallow";
import useKunStore from "./stores/kun";

export default function App() {
  const { setSing, setDance } = useKunStore();

  const { name, sing, dance } = useKunStore(
    useShallow((state) => ({
      name: state.name,
      sing: state.hobbies.sing,
      dance: state.hobbies.dance,
    })),
  );

  // 清空 localStorage, sessionStorage, ...
  const clearKunStorage = () => useKunStore.persist.clearStorage();
  return (
    <>
      <div>name: {name}</div>
      <div>sing: {sing}</div>
      <div>dance: {dance}</div>
      <button onClick={() => setSing(sing + "!")}>setSing</button>
      <button onClick={() => setDance(dance + "!")}>setDance</button>
      <button onClick={() => clearKunStorage()}>清空 localStorage</button>
    </>
  );
}

自定义 logger 中间件

js
export const logger = (fn) => (set, get, storeApi) => {
  const decoratedSet = (...args) => {
    console.log("[set] before", get());
    set(...args);
    console.log("[set] after", get());
  };
  return fn(decoratedSet, get, storeApi);
};
js
import { create } from "zustand";
import { logger } from "@/middlewares/logger";

const useKunStore = create()(
  logger((set) => ({
    name: "kun",

    hobbies: {
      sing: "sing",
      dance: "dance",
    },

    setSing: (newSing) => {
      set((state) => ({
        ...state,
        hobbies: { ...state.hobbies, sing: newSing },
      }));
    },

    setDance: (newDance) => {
      set((state) => ({
        ...state,
        hobbies: { ...state.hobbies, dance: newDance },
      }));
    },
  })),
);

export default useKunStore;

subscribe

subscribe 订阅

js
useStore.subscribe((state) => console.log(state) /** listener */);

subscribe 订阅: state 的任意属性改变时, 都会触发 listener 的调用

  • 组件外部订阅
  • 组件内部订阅: 需要写在 useEffect 中, 并且依赖项数组 deps 是 [] 空数组, 只会在组件挂载后订阅一次, 防止重复订阅

案例

  • 未使用 subscribe 订阅, 每次 age 改变时, 都会触发组件更新
  • 使用 subscribe 订阅, age 改变时, 不会触发组件更新; 只有 isYoung 状态改变时 ,才会触发组件更新
ts
import { create } from "zustand";

interface IUser {
  name: string;
  age: number;
  setName: (newName: string) => void;
  incAge: () => void;
  decAge: () => void;
}

const useUserStore = create<IUser>((set) => ({
  name: "whoami",
  age: 18,
  setName: (newName: string) => set(() => ({ name: newName })),
  incAge: () => set((state) => ({ age: state.age + 1 })),
  decAge: () => set((state) => ({ age: state.age - 1 })),
}));

export default useUserStore;
tsx
import useUserStore from "./stores/user";
import { useShallow } from "zustand/shallow";

function Update() {
  const { setName, incAge, decAge } = useUserStore();
  const name = useUserStore((state) => state.name);
  const age = useUserStore((state) => state.age);

  return (
    <div className="bg-lime-200 flex flex-col gap-5">
      <div>name: {name}</div>
      <div>age: {age}</div>
      <button onClick={() => setName(name + "!")}>setName</button>
      <button onClick={() => incAge()}>incAge</button>
      <button onClick={() => decAge()}>decAge</button>
    </div>
  );
}

export default function App() {
  console.log("App update...");
  const { age } = useUserStore(
    useShallow((state) => ({
      age: state.age,
    })),
  );

  return (
    <>
      <Update />
      {/* 每次 age 改变时, 都会触发组件更新 */}
      <div>{age <= 22 ? "young" : "old"}</div>
    </>
  );
}
tsx
import { useEffect, useState } from "react";
import useUserStore from "./stores/user";

function Update() {
  const { setName, incAge, decAge } = useUserStore();
  const name = useUserStore((state) => state.name);
  const age = useUserStore((state) => state.age);

  return (
    <div className="bg-lime-200 flex flex-col gap-5">
      <div>name: {name}</div>
      <div>age: {age}</div>
      <button onClick={() => setName(name + "!")}>setName</button>
      <button onClick={() => incAge()}>incAge</button>
      <button onClick={() => decAge()}>decAge</button>
    </div>
  );
}

export default function App() {
  console.log("App update...");
  const [isYoung, setIsYoung] = useState(true);

  useEffect(() => {
    // state 的任意属性改变时, 都会触发 listener 的调用
    useUserStore.subscribe((state) => {
      console.log("[useUserStore] listener", state);
      setIsYoung(state.age <= 22);
    });
  });

  return (
    <>
      <Update />
      {/* age 改变时, 不会触发组件更新; 只有 isYoung 状态改变时 ,才会触发组件更新 */}
      <div>{isYoung ? "young" : "old"}</div>
    </>
  );
}

subscribe + subscribeWithSelector 中间件

subscribe + subscribeWithSelector 中间件: state 的指定属性改变时, 才会触发 listener 的调用

案例

  • 未使用 subscribeWithSelector 中间件: state 的任意属性改变时, 都会触发 listener 的调用
  • 使用 subscribeWithSelector 中间件: 仅 state 的 age 属性改变时, 才会触发 listener 的调用
ts
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";

interface IUser {
  name: string;
  age: number;
  setName: (newName: string) => void;
  incAge: () => void;
  decAge: () => void;
}

const useUserStore = create<IUser>()(
  subscribeWithSelector((set) => ({
    name: "whoami",
    age: 18,
    setName: (newName: string) => set(() => ({ name: newName })),
    incAge: () => set((state) => ({ age: state.age + 1 })),
    decAge: () => set((state) => ({ age: state.age - 1 })),
  })),
);

export default useUserStore;
tsx
import { useEffect, useState } from "react";
import useUserStore from "./stores/user";

function Update() {
  const { setName, incAge, decAge } = useUserStore();
  const name = useUserStore((state) => state.name);
  const age = useUserStore((state) => state.age);

  return (
    <div className="bg-lime-200 flex flex-col gap-5">
      <div>name: {name}</div>
      <div>age: {age}</div>
      <button onClick={() => setName(name + "!")}>setName</button>
      <button onClick={() => incAge()}>incAge</button>
      <button onClick={() => decAge()}>decAge</button>
    </div>
  );
}

export default function App() {
  console.log("App update...");
  const [isYoung, setIsYoung] = useState(true);

  useEffect(() => {
    // 仅 state 的 age 属性改变时, 才会触发 listener 的调用
    useUserStore.subscribe(
      (state) => state.age,
      (age, prevAge) => {
        console.log("[useUserStore] listener", age, prevAge);
        setIsYoung(age <= 22);
      },
      {
        equalityFn: (a, b) => a === b,
        fireImmediately: true,
      }
    );
  });

  return (
    <>
      <Update />
      <div>{isYoung ? "young" : "old"}</div>
    </>
  );
}