Skip to content

zustand 搞定 react 中复杂状态管理

https://blog.csdn.net/tiven_/article/details/135149729?spm=1001.2014.3001.5502 Zustand 是一个轻量级的、无依赖的状态库,适用于 React 和函数式编程。 它提供了一个简单、灵活的方式来管理应用程序的状态。 本文就讲讲如何使用 zustand 搞定 react 中复杂状态管理,进而替代 redux 。

官网:https://docs.pmnd.rs/zustand/guides/typescript

zustand使用初体验

  • store 是一个hooks,不需要Provider
  • 推荐使用selectors,可以避免不必要的渲染
  • 基于immutable state model,(react-@reduxjs/toolkit、Valtio 等是基于mutable state model)
  • 支持异步操作

Npm Downloads Trend of State Management Libraries for React

https://npm-compare.com/@reduxjs/toolkit,zustand,recoil,jotai,valtio/#timeRange=THREE_YEARS

一、前言

以 redux 为代表的这类单向数据流状态管理库,都是需要在最外层(根组件)包一个 Provider , Context 中的值都在 Provider 的作用域下有效,这样才能做到数据状态共享。

Zustand 则另辟蹊径,默认不需要 Provider,就想 Vue 中 pinia 状态管理库一样, 直接声明一个 hooks 式的 useStore 后就可以在不同组件中进行调用, 并且保持它们的状态共享和响应式更新。

信息

Zustand 在德语中是 state 状态的意思

二、Zustand 基本使用

定义 Store 数据

text
// src/store/user.js

import { create } from 'zustand';

const initData = {
	userInfo: {},
	token: ''
};

export const useUserStore = create((set, get) => ({
	...initData,
	setUserInfo: (userInfo) => set({ userInfo }),
	getUsername: () => {
		return get().userInfo?.username;
	}
}));

在组件中使用

text
import axios from "axios";
import { useUserStore } from '@/store/user.js'

const Component = () => {
	const { token, setUserInfo, getUsername } = useUserStore()
	const userInfo = useUserStore((state) => state.userInfo)

	const fetchUser = async () => {
		const state = useUserStore.getState()
		const { data } = await axios({
			url: '/xxx',
			headers: {
				'access-token': state.token,
			}
		})
		setUserInfo(data)
	}

	return (
		<div>
			用户:{getUsername()}
		</div>
	)
}

export default Component

注意

在 react hook 组件中函数体内部使用全局的 state,需要使用 getState() 方法获取,否则获取的是初始化的 state 值。

zustand 的 state 是响应式的,所以可以直接在 jsx ui 中使用解构的 state 值 ,但是在非 jsx 中需要使用 getState() 方法获取的状态是非响应式的。

三、Zustand 进阶用法

适用于跨组件数据共享、数据监听操作。

数据监听需要使用 subscribeWithSelector 包裹,否则不能细粒度监听。

text
const unsub1 = useDogStore.subscribe(console.log)

定义 Store 数据

text
// src/store/dialog.js

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

const initData = {
	newDialogVisible: false,
	newFormData: null,
}

export const useDialogStore = create(
	subscribeWithSelector((set, get) => ({
		...initData,
		changeNewDialog(visible, data = null) {
			set({ newDialogVisible: visible, newFormData: data })
		},
	}))
)

设置数据

text
import { forwardRef, useImperativeHandle, useState } from 'react'
import { Button, Form } from 'antd'
import { useDialogStore } from '@/store/dialog.js'

const Dialog = (props, ref) => {
	useImperativeHandle(ref, () => ({
		showModal,
	}))
	const [form] = Form.useForm()
	const { changeNewDialog } = useDialogStore()

	const showModal = (data) => {
		changeNewDialog(true, {})
	}

	return (
		<>
			<Button onClick={showModal} htmlType="submit">新建</Button>
		</>
	)
}

export default forwardRef(Dialog)

监听数据变化

text
import { Breadcrumb } from 'antd'
import Side from './components/Side.jsx'
import List from './components/List.jsx'
import NewDialog from './components/NewDialog.jsx'
import { useEffect, useRef } from 'react'
import { useDialogStore } from '@/store/dialog.js'
import { shallow } from 'zustand/shallow'

const Page = () => {
	const newDialogRef = useRef()
	useEffect(() => {
		// 监听数据变化
		const unsub = useDialogStore.subscribe(
			(state) => [state.newDialogVisible, state.newFormData],
			([visible, data]) => {
				if (visible) {
					// console.log(visible, data)
					newDialogRef.current.showModal(data)
				}
			},
			{ equalityFn: shallow } // 浅比较
		)
		return () => {
			// 取消订阅
			unsub()
		}
	}, [])
	return (
		<>
			<Breadcrumb
				items={[
					{
						title: '首页',
					},
					{
						title: <a href="/">列表</a>,
					},
				]}
			/>
			<div className="border-b-[1px] border-solid border-gray-300 ml-[-20px] mr-[-20px] mt-[15px]"></div>
			<div className="flex justify-between">
				<Side />
				<List />
			</div>
			<NewDialog ref={newDialogRef} />
		</>
	)
}

export default Page

其他用法

text
import { subscribeWithSelector } from 'zustand/middleware'

const useDogStore = create(
	subscribeWithSelector(() => ({ paw: true, snout: true, fur: true }))
)

// Listening to selected changes, in this case when "paw" changes
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log)
// Subscribe also exposes the previous value
const unsub3 = useDogStore.subscribe(
	(state) => state.paw,
	(paw, previousPaw) => console.log(paw, previousPaw)
)
// Subscribe还支持一个可选的 equalityFn 函数
const unsub4 = useDogStore.subscribe(
	(state) => [state.paw, state.fur],
	console.log,
	{ equalityFn: shallow }
)
// Subscribe and fire immediately
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
	fireImmediately: true,
})

四、在 React 组件外使用

在 axios 或路由守卫中通常需要获取/设置全局的 token 和用户信息,使用 zustand 可以这样做:

获取状态

text
// react 组件外直接取值
const token = useUserStore.getState().token

设置更新状态

text
// react 组件外更新值
useUserStore.setState({ userInfo: data })

参考文档:

https://www.npmjs.com/package/zustandhttps://mp.weixin.qq.com/s/bqPJWzWWBk_dnKUBq0btPghttps://zhuanlan.zhihu.com/p/591981209https://www.jianshu.com/p/516c85c50da8

React状态管理后起之秀Zustand

zustand的基本使用

text
import { create } from 'zustand'

const useStore = create((set) => ({
	count: 1,
	inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
	const { count, inc } = useStore()
	return (
		<div>
			<span>{count}</span>
			<button onClick={inc}>one up</button>
		</div>
	)
}

注意

防止重新渲染,比如更新某个值相邻的操作之间值不变也触发了重新渲染,这种就需要加个处理

text
import { shallow } from "zustand/shallow";

// 原来是这样
const counter = useCounter((state) => state.counter);

// 优化之后是这样, 据说这样就会触发比较:  (previousCounter, nextCounter) => previousCounter === nextCounter;
const counter = useCounter((state) => state.counter, shallow);

支持异步操作,比如网络请求刷新状态

text
const useCounter = create((set) => {
	return {
		counter: 0,
		incrCounter: async () => {
			const { data } = await axios.get("/counter");
			set({
				counter: data.counter,
			});
		},
	};
});

模块化使用 (没有必要!)

  • personSlice.js
text
//需要管理的状态变量以及操作方法均放在一个箭头函数里返回一个对象
export const PersonSlice = (set) => ({
	name: 'zustand',
	age: 18,
	add: () => set((state) => ({ age: state.age + 1 })),
	resetZero: () => set({ age: 0 }),
});
  • store.js
text
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

///将不同类型的状态按职责区分在不同的文件然后导出汇聚到这里处理, 就是模块化的意思
import { PersonSlice } from './personSlice.js';

/// 这里定义状态和操作方法
// const createStore = (set) => ({
//   count: 0,
//   //网络请求异步操作
//   fishies: {},
//   fetch: async (pond) => {
//     const response = await fetch(pond);
//     set({ fishies: await response.json() });
//   },
// });

///调试和持久化
// devtools 是对调试工具的支持
// persist 是保存持久化的中间件,刷新时添加到状态的数据不会丢失 是存在localStorage里的 name就是key value就是这个store的对象
//不需要时可以加个方法进行清除localStorage的数据 
// 这里只是一个举例,实际开发中store可以建多个,可以按持久化和非持久化来划,也可按模块划分
const useStore = create(
	devtools(
		persist(
			(...a) => ({
				//类似Redux Slice切片  切片获取状态管理的对象进行解构组装
				// ...createSlice(...a),
				...PersonSlice(...a),
			}),
			{
				name: 'store', //唯一key      
				getStorage: () => sessionStorage, // (可选)默认使用'localStorage'
			}
		)
	)
);
export default useStore;
  • index.jsx
text
import React from 'react';
import useAppStore from '../store.js';

/// 状态管理示例
function StoreExample() {
	//取出状态管理的变量进行展示 
	//const {age,name} = useAppStore();
	const age = useAppStore((state) => state.age);
	const name = useAppStore((state) => state.name);
	return (
		<div>
			<div>age: {age}</div>
			<div>name: {name}</div>
		</div>
	);
}

function ActionExample() {
	//取出操作方法进行调用 
	//const { add, resetZero } = useAppStore();
	const add = useAppStore((state) => state.add);
	const resetZero = useAppStore((state) => state.resetZero);


	return (
		<div>
			<button onClick={add}>Add</button>
			<button onClick={resetZero}>Reset</button>
		</div>
	);
}

export default function Index() {
	return (
		<>
			<StoreExample/>
			<ActionExample/>
		</>
	);
}

一个todos的案例

text
import create from "zustand";

//定义store
const useStore = create((set) => ({
	todos: [],
	addTodo: (text) =>
		set((state) => ({
			todos: [
				...state.todos,
				{
					id: Date.now(),
					text,
					completed: false,
				},
			],
		})),
	toggleTodo: (id) =>
		set((state) => ({
			todos: state.todos.map((todo) =>
				todo.id === id ? { ...todo, completed: !todo.completed } : todo
			),
		})),
	deleteTodo: (id) =>
		set((state) => ({
			todos: state.todos.filter((todo) => todo.id !== id),
		})),
}));

export default useStore;


//显示组件
const DisplayTodos = () => {
	const { todos, deleteTodo } = useStore((state) => {
		return { todos: state.todos, deleteTodo: state.deleteTodo };
	});

	return (
		<ul>
			{todos.map((todo) => (
				<li
					key={todo.id}
					style={{
						textDecoration: todo.completed
							? "line-through"
							: "none",
					}}
					onClick={() => toggleTodo(todo.id)}
				>
					{todo.text}
					<button onClick={() => deleteTodo(todo.id)}>Delete</button>
				</li>
			))}
		</ul>
	);
};

export default DisplayTodos;


//添加todos表单
const TodosControl = () => {
	const addTodo = useStore((state) => state.addTodo);
	const [text, setText] = useState("");

	function handleSubmit(e) {
		e.preventDefault();
		addTodo(text);
		setText("");
	}

	return (
		<form onSubmit={handleSubmit}>
			<input
				type="text"
				value={text}
				onChange={(e) => setText(e.target.value)}
			/>
			<button type="submit">Add</button>
		</form>
	);
};

export default TodosControl;


//todos
const App = () => {
	return (
		<>
			<DisplayTodos/>
			<TodosControl/>
		</>
	);
};

export default App;

JS代码案例

Case 1 简单使用

text
import create from "zustand";

export const useZustand = create((set) => ({
	counter: 0,
	buttonTitle: "Zustand : ",

	increment: () =>
		set((state) => {
			let increasingValue = state.counter + 1;
			state.counter >= 10 && (increasingValue = 10);
			return { counter: increasingValue };
		}),

	decrement: () =>
		set((state) => {
			let decreasingValue = state.counter - 1;
			state.counter <= 0 && (decreasingValue = 0);
			return { counter: decreasingValue };
		}),

	reset: () => set({ counter: 0 }),
}));

import { useZustand } from "../stores/zustandStore";

const CounterButtonZustand = () => {
	//取出使用 
	const buttonTitle = useZustand((state) => state.buttonTitle);
	const counter = useZustand((state) => state.counter);
	const handleIncrement = useZustand((state) => state.increment);
	const handleDecrement = useZustand((state) => state.decrement);

	useEffect(() => {
		console.log("zustand: " + counter);
	}, [counter]);

	return (
		<div className="counter">
			<h2 className="counter-title">{buttonTitle}</h2>

			<div className="counter-value-container">
				<button onClick={handleDecrement}>
					<FontAwesomeIcon icon={faMinus} size="4x" className="icons"/>
				</button>

				<DisplayCounter value={counter}/>

				<button onClick={handleIncrement}>
					<FontAwesomeIcon icon={faPlus} size="4x" className="icons"/>
				</button>
			</div>
		</div>
	);
};

Case2 区分不同场景的使用

text
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'

//暗黑模式切换 适用于某种全局的状态管理
let settingsStore = (set) => ({
	dark: false,
	toggleDarkMode: () => set((state) => ({ dark: !state.dark })),
})

let peopleStore = (set) => ({
	people: ['John Doe', 'Jane Doe'],
	addPerson: (person) =>
		set((state) => ({ people: [...state.people, person] })),
})

settingsStore = devtools(settingsStore)
//持久化
settingsStore = persist(settingsStore, { name: 'user_settings' })

//不需要持久化
peopleStore = devtools(peopleStore)

export const useSettingsStore = create(settingsStore)
export const usePeopleStore = create(peopleStore)


function App() {
	const toggleDarkMode = useSettingsStore((state) => state.toggleDarkMode)
	const dark = useSettingsStore((state) => state.dark)

	useEffect(() => {
		if (dark) {
			document.querySelector('body').classList.add('dark')
		} else {
			document.querySelector('body').classList.remove('dark')
		}
	}, [dark])

	return (
		<div className="App">
			<button onClick={toggleDarkMode}>Toggle Dark Mode</button>
			<p>People</p>
			<Input/>
			<People/>
		</div>
	)
}

Case3 装饰器模式

高度封装

  • useUsersStore.js
text
import create from "zustand"

const initialState = {
	users: [],
	loading: false,
	error: "",
}

const useUsersStore = create((set) => ({
	users: initialState.users,
	loading: initialState.loading,
	error: initialState.error,

	fetchUsers: async () => {
		set((state) => ({ ...state, loading: true }))
		try {
			const res = await fetch("https://jsonplaceholder.typicode.com/users")
			const users = await res.json()
			set((state) => ({ ...state, error: "", users }))
		} catch (error) {
			set((state) => ({
				...state,
				error: error.message,
			}))
		} finally {
			set((state) => ({
				...state,
				loading: false,
			}))
		}
	},

	// In our example we only need to fetch the users, but you'd probably want to define other methods here
	addUser: async (user) => {
	},
	updateUser: async (user) => {
	},
	deleteUser: async (id) => {
	},
}))

export default useUsersStore
  • useUsersFacade.js

使用用户界面

text
import useUsersStore from "../store/useUsersStore"

import shallow from "zustand/shallow"

const useUsersFacade = () => {
	const { users, loading, error, fetchUsers } = useUsersStore(
		(state) => ({
			users: state.users,
			loading: state.loading,
			error: state.error,
			fetchUsers: state.fetchUsers,
		}),
		shallow
	)

	return { users, loading, error, fetchUsers }
}

export default useUsersFacade
  • Users.js
text
import { useEffect } from "react"

import useUsersFacade from "../facades/useUsersFacade"

const Users = () => {
	const { users, loading, error, fetchUsers } = useUsersFacade()

	useEffect(() => fetchUsers(), [])

	return (
		<div>
			{loading && <p data-testid="loading">Loading...</p>}
			{error && <p data-testid="error">{error}</p>}
			{users?.length > 0 && (
				<ul data-testid="users-list">
					{users.map((user) => (
						<li key={user.id}>{user.name}</li>
					))}
				</ul>
			)}
		</div>
	)
}

export default Users


import Users from "./pages/Users"

function App() {
	return <Users/>
}

export default App

Case4 增删改经典案例

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


const courseStore = (set) => ({
	courses: [],
	addCourse: (course) => {
		set((state) => ({
			courses: [...state.courses, course]
		}))
	},
	removeCourse: (id) => {
		set((state) => ({
			courses: state.courses.filter((c) => c.id !== id)
		}))
	},
	toggleCourseState: (id) => {
		set((state) => ({
			courses: state.courses.map((c) => c.id === id ? { ...c, completed: !c.completed } : c)
		}))
	}

})

const useCourseStore = create(devtools(
	persist(courseStore, {
		name: 'courses'
	})
))

export default useCourseStore;
CourseForm.jsx
text
import React, { useState } from 'react';
import useCourseStore from '../app/courseStore';

const CourseForm = () => {
	const courses = useCourseStore((state) => state.courses)
	const addCourse = useCourseStore((state) => state.addCourse);
	const [courseTitle, setCourseTitle] = useState('')


	const handleFormSubmit = () => {
		if (courseTitle) {
			addCourse({
				id: courses.length + 1,
				title: courseTitle,
				completed: false
			});
			setCourseTitle('');
		}
	}


	return (
		<div className="form-container">
			<input value={courseTitle} onChange={(e) => setCourseTitle(e.target.value)} type="text" className="form-input"/>
			<button onClick={() => handleFormSubmit()} className="form-submit-btn">Add Course</button>
		</div>
	)
}

export default CourseForm
CourseList.jsx
text
import React from 'react'
import useCourseStore from '../app/courseStore'

const CourseList = () => {
	const { courses, removeCourse, toggleCourseState } = useCourseStore((state) => state);
	return (
		<>
			<ul>
				{courses.map((course, index) => (
					<li key={index} className='course-item' style={{ backgroundColor: course.completed ? '#b1f59d' : 'white' }}>
						<span className="course-item-col-1">
							<input type="checkbox" checked={course.completed} onChange={() => toggleCourseState(course.id)}/>
						</span>
						<span style={{ color: 'black' }}>{course?.title}</span>
						<button className='delete-btn' onClick={(e) => removeCourse(course.id)}>Delete</button>
					</li>
				))}
			</ul>
		</>
	)
}

export default CourseList

Case5

text
import create from 'zustand';

const actions = (set) => ({
	increase: () => set(state => ({
		count: state.count + 1
	})),
	clear: () => set({
		count: 0
	}),
});

const useStore = create(set => ({
	count: 0,
	...actions(set),
}));

export default useStore;

//提前导出
export const selectCount = (state) => (
	state.count
);

export const selectIncrease = (state) => (
	state.increase
);

export const selectClear = (state) => (
	state.clear
);


import React from "react";
import useStore from '../store/store';
import { selectIncrease, selectClear } from '../store/selectors';

function Buttons() {
	//类似于这种 提前导出的话就直接写到括号里 调用不用写箭头函数 但是写哪里不是写 反正跑不了的都要写的 直接获取全部的话貌似对性能有所损耗
	//const toggleDarkMode = useSettingsStore((state) => state.toggleDarkMode)


	const increase = useStore(selectIncrease);
	const clear = useStore(selectClear);

	return (
		<React.Fragment>
			<button onClick={() => increase()}>Increase</button>
			<button onClick={() => clear()}>Reset</button>
		</React.Fragment>
	);
}

export default Buttons;

Case6 可变的/可修改的 使用immmer

store.js

text
import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
	// 剧集
	kdramas: [
		{
			id: Math.floor(Math.random() * 100),
			name: "River Where the Moon Rises",
		},
		{
			id: Math.floor(Math.random() * 100),
			name: "The Crowned Clown",
		},
	],
	addDrama: (payload) =>
		set(
			produce((draft) => {
				draft.kdramas.push({
					id: Math.floor(Math.random() * 100),
					name: payload,
				});
			})
		),
	removeDrama: (payload) =>
		set(
			produce((draft) => {
				const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
				draft.kdramas.splice(dramaIndex, 1);
			})
		),
	patchDrama: (payload) =>
		set(
			produce((draft) => {
				const drama = draft.kdramas.find((el) => el.id === payload.id);
				drama.name = payload.name;
			})
		),
}));
text
import React, { useState } from "react";

import { useStore } from "./store";

export default function App() {
	const { kdramas, addDrama, removeDrama, patchDrama } = useStore();
	const [input, setInput] = useState("");
	const [isEdit, setIsEdit] = useState(false);
	const [update, setUpdate] = useState({
		id: null,
		name: "",
	});
	const addDramaHandler = (e) => {
		e.preventDefault();
		addDrama(input);
		setInput("");
	};
	const updateClickHandler = (drama) => {
		setIsEdit(true);
		setUpdate({
			id: drama.id,
			name: drama.name,
		});
	};
	const patchDramaHandler = (e) => {
		e.preventDefault();
		patchDrama(update);
		setUpdate({
			id: null,
			name: "",
		});
		setIsEdit(false);
	};
	return (
		<div>
			<div style={{ display: "flex" }}>
				<input
					value={input}
					onChange={(e) => setInput(e.target.value)}
					placeholder="Add new Korean Drama"
				/>
				<button onClick={addDramaHandler}>Add</button>
			</div>
			<br/>
			{kdramas.map((el) => (
				<div key={el.id}>
					<h1>{el.name}</h1>
					<button onClick={() => removeDrama(el.id)}>
						<i className="ti ti-trash"></i>
					</button>
					{" "}
					<button onClick={() => updateClickHandler(el)}>
						<i className="ti ti-edit-circle"></i>
					</button>
				</div>
			))}
			<br/>
			{isEdit && (
				<div style={{ display: "flex", flexDirection: "column" }}>
					<label>Patch Korean Drama</label>
					<div style={{ display: "flex" }}>
						<input
							value={update.name}
							onChange={(e) => setUpdate({ ...update, name: e.target.value })}
						/>
						<button onClick={patchDramaHandler}>Patch</button>
					</div>
				</div>
			)}
		</div>
	);
}

React Zustand状态管理库 B站视频

https://www.bilibili.com/video/BV1JC4y1o7eg/

视频github代码在这儿, 每课对应一个commit: https://github.com/niftybytes4u/react_zustand_tutorial/tree/8d0256a8d7ff2190296640a1d8b7976f313600bc

react zustand 库: https://github.com/pmndrs/zustand

Why zustand typescript implementation is so ugly https://blog.axlight.com/posts/why-zustand-typescript-implementation-is-so-ugly/

middlewareTypes.test.tsx: https://github.com/pmndrs/zustand/blob/main/tests/middlewareTypes.test.tsx

翻译文档 https://zhuanlan.zhihu.com/p/475571377

ts使用案例

  • src/stores/foodStore.ts
ts
import { create } from "zustand";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";

const initialFoodValue = {
	fish: 0,
	mouse: 0,
};

export const useFoodStore = create<typeof initialFoodValue>()(
	devtools(
		subscribeWithSelector(
			persist(() => initialFoodValue, { name: "food store" })
		),
		{ name: "food store" }
	)
);

export const addOneFish = () =>
	useFoodStore.setState((state) => ({ fish: state.fish + 1 }));

export const removeOneFish = () =>
	useFoodStore.setState((state) => ({ fish: state.fish - 1 }));
export const removeAllFish = () => useFoodStore.setState({ fish: 0 });
  • src/components/FoodBox.tsx
tsx
import {
	addOneFish,
	removeAllFish,
	removeOneFish,
	useFoodStore,
} from "../stores/foodStore";

export const FoodBox = () => {
	// const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();

	const fish = useFoodStore((state) => state.fish);
	// const fish = useFoodStore.getState().fish; // non-reactive

	const add5Fish = () => {
		useFoodStore.setState((state) => ({
			fish: state.fish + 5,
		}));
	};
	return (
		<div className="box">
			<h1>Food Box</h1>
			<p>fish: {fish}</p>
			<div>
				<button onClick={addOneFish}>add one fish</button>
				<button onClick={removeOneFish}>remove one fish</button>
				<button onClick={removeAllFish}>remove all fish</button>

				<button onClick={add5Fish}>add 5 fish</button>
			</div>
		</div>
	);
};

src/components/BearBox.tsx

tsx
import { useEffect, useState } from "react";
import { shallow } from "zustand/shallow";
import { useBearStore } from "../stores/bearStore";
import { useFoodStore } from "../stores/foodStore";

export const BearBox = () => {
	// const bears = useBearStore((state) => state.bears);
	// const increasePopulation = useBearStore((state) => state.increasePopulation);
	// const removeAllBears = useBearStore((state) => state.removeAllBears);
	const { bears, increasePopulation, removeAllBears } = useBearStore();

	const [bgColor, setBgColor] = useState<
		"lightgreen" | "lightpink" | undefined
	>(useFoodStore.getState().fish > 5 ? "lightgreen" : "lightpink");

	// const fish = useFoodStore((state) => state.fish);

	useEffect(() => {
		// const unsub = useFoodStore.subscribe((state, prevState) => {
		//   if (prevState.fish <= 5 && state.fish > 5) {
		//     setBgColor("lightgreen");
		//   } else if (prevState.fish > 5 && state.fish <= 5) {
		//     setBgColor("lightpink");
		//   }
		// });

		const unsub = useFoodStore.subscribe(
			(state) => state.fish,
			(fish, prevFish) => {
				// if (fish == prevFish) {
				//   if (fish <= 5) {
				//     setBgColor("lightpink");
				//   } else {
				//     setBgColor("lightgreen");
				//   }
				// }

				if (prevFish <= 5 && fish > 5) {
					setBgColor("lightgreen");
				} else if (prevFish > 5 && fish <= 5) {
					setBgColor("lightpink");
				}
			},
			{
				equalityFn: shallow,
				fireImmediately: true,
			}
		);

		return unsub;
	}, []);

	return (
		<div className="box" style={{ backgroundColor: bgColor }}>
			<h1>Bear Box</h1>
			<p>bears: {bears}</p>
			<p>{Math.random()}</p>
			<div>
				<button onClick={increasePopulation}>add bear</button>
				<button onClick={removeAllBears}>remove all bears</button>
				<button onClick={useBearStore.persist.clearStorage}>
					clear storage
				</button>
			</div>
		</div>
	);
};

注意中间件的嵌套层级

Also, we recommend using devtools middleware as last as possible. For example, when you use it with immer as a middleware, it should be immer(devtools(...)) and not devtools(immer(...)). This is becausedevtools mutates the setState and adds a type parameter on it, which could get lost if other middlewares (like immer) also mutate setState before devtools. Hence using devtools at the end makes sure that no middlewares mutate setState before it.

ts
export const useCatStore = createSelectors(
	create<TCatStoreState>()(
		immer(
			devtools(
				subscribeWithSelector(
					persist(createCatSlice, {
						name: "cat store",
					})
				),
				{
					enabled: true,
					name: "cat store",
				}
			)
		)
	)
);

注意

Please keep in mind you should only apply middlewares in the combined store. Applying them inside individual slices can lead to unexpected issues.

zustand 传值

https://blog.csdn.net/qq_53479087/article/details/135651807?spm=1001.2014.3001.5502

useGlobalStore + persist

ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface GlobalState {
	primaryColor: string
	setColor: (color: string) => void
}

const useGlobalStore = create<GlobalState>()(
	persist(
		(set) => ({
			primaryColor: '#00b96b',
			setColor: (color) => set(() => ({ primaryColor: color })),
		}),
		{
			name: 'primaryColor',
			// partialize 过滤属性,存储哪些字段到localStorage
			partialize: (state) =>
				Object.fromEntries(Object.entries(state).filter(([key]) => ['primaryColor'].includes(key))),
		}
	)
)

export default useGlobalStore

一次取出多个值和方法

ts
import { shallow } from 'zustand/shallow'
import { useGlobalStore } from '~/stores'

const { primaryColor, setColor } = useGlobalStore(
	(state) => ({
		primaryColor: state.primaryColor,
		setColor: state.setColor,
	}),
	shallow
)

const changeMainColor = useDebounceFn((e: React.ChangeEvent<HTMLInputElement>) => {
	setColor(e.target.value)
}, 500)

一个一个的取值

ts
const counter = useCounterStore((state) => state.counter)
const increase = useCounterStore((state) => state.increase)

在纯js环境中取值 和 清除值

ts
const token = useUserInfoStore.getState().userInfo?.token

useUserInfoStore.setState({ userInfo: null })

const add5Fish = () => {
	useFoodStore.setState((state) => ({
		fish: state.fish + 5,
	}));
};

useBearStore + persist

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

interface TBearStoreState {
	bears: number;
	color: string;
	size: string;
	increasePopulation: () => void;
	removeAllBears: () => void;
}

export const useBearStore = create<TBearStoreState>()(
	persist(
		(set) => ({
			bears: 0,
			color: "red",
			size: "big",
			increasePopulation: () =>
				set((state) => ({
					bears: state.bears + 1,
				})),
			removeAllBears: () => set({ bears: 0 }),
		}),
		{
			name: "bear store",
			partialize: (state) =>
				Object.fromEntries(
					Object.entries(state).filter(
						([key]) => !["size", "color"].includes(key)
					)
				),
		}
	)
);

取值方式3

ts
  const { bears, increasePopulation, removeAllBears } = useBearStore();

取出方法

ts
  const [increaseBigCats, increaseSmallCats] = useCatStore(
	(state) => [state.increaseBigCats, state.increaseSmallCats],
	shallow
);

subscribeWithSelector + devtools + 直接导出函数

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

const initialFoodValue = {
	fish: 0,
	mouse: 0,
};

export const useFoodStore = create<typeof initialFoodValue>()(
	devtools(
		subscribeWithSelector(
			persist(() => initialFoodValue, { name: "food store" })
		),
		{ name: "food store" }
	)
);

export const addOneFish = () =>
	useFoodStore.setState((state) => ({ fish: state.fish + 1 }));

export const removeOneFish = () =>
	useFoodStore.setState((state) => ({ fish: state.fish - 1 }));
export const removeAllFish = () => useFoodStore.setState({ fish: 0 });

直接使用函数

tsx
import {
	addOneFish,
	removeAllFish,
	removeOneFish,
	useFoodStore,
} from "../stores/foodStore";

export const FoodBox = () => {
	// const { fish, addOneFish, removeOneFish, removeAllFish } = useFoodStore();

	const fish = useFoodStore((state) => state.fish);
	// const fish = useFoodStore.getState().fish; // non-reactive

	const add5Fish = () => {
		useFoodStore.setState((state) => ({
			fish: state.fish + 5,
		}));
	};
	return (
		<div className="box">
			<h1>Food Box</h1>
			<p>fish: {fish}</p>
			<div>
				<button onClick={addOneFish}>add one fish</button>
				<button onClick={removeOneFish}>remove one fish</button>
				<button onClick={removeAllFish}>remove all fish</button>

				<button onClick={add5Fish}>add 5 fish</button>
			</div>
		</div>
	);
};

subscribe 订阅

ts
useEffect(() => {
	// const unsub = useFoodStore.subscribe((state, prevState) => {
	//   if (prevState.fish <= 5 && state.fish > 5) {
	//     setBgColor("lightgreen");
	//   } else if (prevState.fish > 5 && state.fish <= 5) {
	//     setBgColor("lightpink");
	//   }
	// });

	const unsub = useFoodStore.subscribe(
		(state) => state.fish,
		(fish, prevFish) => {
			// if (fish == prevFish) {
			//   if (fish <= 5) {
			//     setBgColor("lightpink");
			//   } else {
			//     setBgColor("lightgreen");
			//   }
			// }

			if (prevFish <= 5 && fish > 5) {
				setBgColor("lightgreen");
			} else if (prevFish > 5 && fish <= 5) {
				setBgColor("lightpink");
			}
		},
		{
			equalityFn: shallow,
			fireImmediately: true,
		}
	);

	return unsub;
}, []);

这段代码是使用 React Hooks(结合 Zustand 库)监听状态变化并据此更新组件样式背景色的示例。下面是详细的解释:

  1. useEffect 是 React 的内置 Hook,用于处理副作用操作,如订阅事件、定时任务、DOM 更新等。在这个例子中,useEffect 的第二个参数为空数组 [],意味着这个副作用仅在组件挂载时执行一次。

  2. useFoodStore.subscribe 是来自 Zustand 库的方法,用于订阅状态 store 的变化。当 store 中的状态(state)发生变化时,提供的回调函数会被执行。

                                  	```javascript
                                 	 const unsub = useFoodStore.subscribe(
                                 		 (state) => state.fish,
                                 		 (fish, prevFish) => {...}
                                 		 // 其他配置项
                                 	 );
                                 	 ```
    
                                 	第一个参数是选择器函数,它返回 store 中我们需要关注的特定部分(这里是 `state.fish`,鱼的数量)。
    
                                 	第二个参数是订阅的回调函数,它接收两个参数:当前的鱼的数量 `fish` 和前一个鱼的数量 `prevFish`。当 `fish`
                                 	的值发生变化时,这个回调函数会被调用。
    
                                 	回调函数内的逻辑是根据鱼的数量变化来切换背景颜色。当鱼的数量从少于等于5变为多于5时,背景色设置为 "lightgreen"
                                 	;反之,当鱼的数量从多于5变为少于等于5时,背景色设置为 "lightpink"。
    
  3. equalityFn: shallow 表示在比较新旧状态时使用浅比较(shallow equality check),即只有当引用地址改变时才认为状态发生了变化。

  4. fireImmediately: true 表示订阅后立即执行一次回调函数,无论状态是否已经改变。

  5. 最后,useEffect 的返回函数 unsub 是一个取消订阅的方法,当组件卸载时会自动执行,以防止内存泄漏,确保不再接收无效的状态更新通知。

总结:这段代码在组件挂载时订阅 useFoodStore 中的 fish 状态变化,根据鱼的数量变化动态更改组件的背景色,并在组件卸载时清理订阅。

最佳实践

store 是定义state和actions的地方 业务组件中是消费store中的商品(state\action)的地方

总结一下,Zustand 通过以下方式来避免无效渲染:

选择器:通过允许你订阅特定的状态片段,仅当这些片段发生变化时才触发组件的重新渲染。 引用相等性检查:默认情况下,选择器返回的状态片段会通过严格相等性检查来决定是否需要重新渲染。 浅层比较:使用 useShallow 或其他浅层比较函数来对选择器返回的对象进行浅层属性比较,从而进一步减少不必要的渲染。

text
相关中间件等应用顺序(固定写法):create(immer(devtools(subscribeWithSelector(persist((set, get) => ({}))))))

使用useShallowSelector

关于zustand的一些最佳实践 https://www.pipipi.net/31839.html

tsx
import { useShallow } from 'zustand/react/shallow';
import useConfigStore from './store';

const Theme = () => {

	const { theme, setTheme } = useConfigStore(
		useShallow(state => ({
			theme: state.theme,
			setTheme: state.setTheme,
		}))
	);
	console.log('theme render');

	return (
		<div>
			<div>{theme} </div>
			<button
				onClick={() =>
					setTheme(theme === 'light' ? 'dark' : 'light')
				}>
				切换
			</button>
		</div>
	)
}

export default Theme;

封装了一个useSelector方法,使用起来更简单一点。

ts
import { pick } from 'lodash-es';

import { useRef } from 'react';
import { shallow } from 'zustand/shallow';

type Pick<T, K extends keyof T> = {
	[P in K]: T[P];
};

type Many<T> = T | readonly T[];

export function useSelector<S extends object, P extends keyof S>(
	paths: Many<P>
): (state: S) => Pick<S, P> {
	const prev = useRef<Pick<S, P>>({} as Pick<S, P>);

	return (state: S) => {
		if (state) {
			const next = pick(state, paths);
			return shallow(prev.current, next) ? prev.current : (prev.current = next);
		}
		return prev.current;
	};
}

useSelector主要使用了lodash里的pick方法,然后使用了zustand对外暴露的shallow方法,进行对象浅比较。

tsx
import useConfigStore from './store';
import { useSelector } from './use-selector';

const Theme = () => {

	const { theme, setTheme } = useConfigStore(
		useSelector(['theme', 'setTheme'])
	);

	console.log('theme render');

	return (
		<div>
			<div>{theme}</div>
			<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
		</div>
	)
}

export default Theme;

封装的useSelector只需要传入对外暴露的字符串数组就行了,不用再写方法了,省了很多代码,同时还保留了ts的类型推断。

useShallow 是从 Zustand 提供的 /shallow 路径中导入的一个工具函数,它对返回的对象进行浅层比较,如果对象的顶层属性和之前的状态一样,即使引用不同,也不会重新渲染组件。

V5 版本的写法

ts
import { useShallow } from "zustand/shallow";

const { nuts, honey } = useStore(
	(state) => ({
		nuts: state.nuts,
		honey: state.honey,
	}),
	useShallow,
);

V4 版本的写法

tsx
import { shallow } from "zustand/shallow";
import useAppleStore from "@/stores/appleStore.ts";
/* v5 版本的写法 */
// import { useShallow } from "zustand/react/shallow";

const Child = () => {
	console.log("Child render");

	const { count, add, remove } = useAppleStore(
		(state) => ({
			count: state.count,
			add: state.add,
			remove: state.remove,
		}),
		shallow,
	);
	return (
		<div>
			Child
			<div>Count: {count}</div>
			<button onClick={add}>Add</button>
			<button onClick={remove}>Remove</button>
		</div>
	);
};

const Child2 = () => {
	console.log("Child2 render");

	/* v5 版本的写法 */
	// const { price, color } = useAppleStore(
	// 	useShallow((state) => ({ price: state.prices, color: state.color })),
	// );

	const { price, color } = useAppleStore(
		(state) => ({ price: state.prices, color: state.color }),
		shallow,
	);
	return (
		<div>
			Child2
			<div>Price: {price}</div>
			<div>Color: {color}</div>
		</div>
	);
};

const UseShallowDemo = () => {
	console.log("UseShallowDemo render");

	return (
		<div>
			<Child />

			<hr />
			<Child2 />
		</div>
	);
};

export default UseShallowDemo;

多实例

zustand的数据默认是全局的,也就是说每个组件访问的数据都是同一个,那如果写了一个组件,这个组件在多个地方使用,如果用默认方式,后面的数据会覆盖掉前面的,这个不是我们想要的。

为了解决这个问题,官方推荐这样做:

tsx
import React, { createContext, useRef } from 'react';
import { StoreApi, createStore } from 'zustand';

interface State {
	theme: string;
	lang: string;
}

interface Action {
	setTheme: (theme: string) => void;
	setLang: (lang: string) => void /*  */
	;
}


export const StoreContext = createContext<StoreApi<State & Action>>(
	{} as StoreApi<State & Action>
);

export const StoreProvider = ({ children }: any) => {
	const storeRef = useRef<StoreApi<State & Action>>();

	if (!storeRef.current) {
		storeRef.current = createStore<State & Action>((set) => ({
			theme: 'light',
			lang: 'zh-CN',
			setLang: (lang: string) => set({ lang }),
			setTheme: (theme: string) => set({ theme }),
		}));
	}

	return React.createElement(
		StoreContext.Provider,
		{ value: storeRef.current },
		children
	);
};

使用了React的context

使用Theme组件来模拟两个实例,使用StoreProvider包裹Theme组件

tsx
import './App.css'
import { StoreProvider } from './store'
import Theme from './theme'

function App() {

	return (
		<>
			<StoreProvider>
				<Theme />
			</StoreProvider>
			<StoreProvider>
				<Theme />
			</StoreProvider>
		</>
	)
}

export default App

Theme组件

tsx
import { useContext } from 'react';
import { useStore } from 'zustand';
import { StoreContext } from './store';

const Theme = () => {

	const store = useContext(StoreContext);
	const { theme, setTheme } = useStore(store);

	return (
		<div>
			<div>{theme}</div>
			<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
		</div>
	)
}

export default Theme;

Contributors

作者:Long Mo
字数统计:5.4k 字
阅读时长:26 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs