RavelloH's Blog

LOADing...



Zustand实现React全局状态管理

zustand-react-global-state-management

技术/设计 6167

zustandreactnextjsjavascript


前言

之前发现在用Nextjs写组件间通信的时候写的非常折磨,要靠传递给共同的父组件,这样父组件传数据和回调函数给子组件,子组件再调用这样来互相传递,只能说这样的写法我学会了就不会想去用了。

随后我尝试兄弟组件间状态提升,虽然不那么麻烦了,但是共同的父组件还是要写一推,你还要去判断某组件和某组件在通信的时候谁是共同的父组件,如果中间跨了很多层就要逐级往下传props,还是很麻烦。

虽说Context API能一定程度上解决往下传太麻烦这个问题,但仍然不够简便,我想要的是在任意一个组件中,都能简单的调用某个/某些组件里的功能。

这样的话有两个选择,一是直接用事件总线(EventEmitter),或者用Zustand 或 Redux库。其中用事件总线的话,多种调用情况的处理就要用到多个useEffect:

useEffect(() => {
    eventBus.on("update", setData);
    return () => eventBus.off("update", setData);
}, []);

这样其实如果同一组件要处理多个事件的话,写起来就更麻烦,于是我自己用Zustand写了个broadcast系统,用起来就非常方便了。

文章写到一半我觉得单个Broadcast的作用还是太局限了,于是写了三套,一劳永逸了,作用如下

Hook 名称 主要用途 机制 适用场景
useBroadcast 全局广播 一次性通知所有监听者 适用于简单的全局消息通知
useEvent 事件管理 支持多种事件及监听器 适用于更复杂的事件管理
useFunction 动态函数注册 存储和调用命名函数 适用于插件系统或动态调用函数

具体的函数都放在文末了,或者你也可以在Github Gist中查看。Zustand实现React全局状态管理

zustand-gist zustand-gist

示例

广播

对于接收方:

import { useBroadcast } from '@/store/useBroadcast';
import { useEffect } from 'react';

export default function Receiver() {
	const { registerCallback, unregisterCallback } = useBroadcast();

	useEffect(() => {
	    const handleMessage = (message) => {
	        if (message.action == 'A') {
		        // DO A
	        }
	        if (message.action == 'B') {
	            // DO B
	        }
	    };
	    registerCallback(handleMessage);
	    return () => {
	        unregisterCallback();
	    };
	}, [registerCallback, unregisterCallback]);

	return;
}

对于发送方:

import { useBroadcast } from '@/store/useBroadcast';

export default function Sender() {
	const { broadcast } = useBroadcast();
	return <button onClick={()=>broadcast({action:"A"})}></button>
}

简直是太优雅了,唯一的缺点是你可能得记住每个action是用来干什么的,毕竟这个注册了也没法给你关联起来。

示例中我是直接传递了个Object作为消息,实际上虽然传什么类型都行,我也推荐你统一传个Object,这样可用于指定的动作更多。

事件

对于接收方(监听事件)

import { useEvent } from '@/store/useEvent';
import { useEffect } from 'react';

export default function Receiver() {
    const { on, off } = useEvent();

    useEffect(() => {
        const handleCustomEvent = (data) => {
            if (data.action === 'A') {
                // DO A
            }
            if (data.action === 'B') {
                // DO B
            }
        };

        on('customEvent', handleCustomEvent);
      
        return () => {
            off('customEvent', handleCustomEvent);
        };
    }, [on, off]);

    return null;
}

对于发送方(触发事件)

import { useEvent } from '@/store/useEvent';

export default function Sender() {
    const { emit } = useEvent();
  
    return <button onClick={() => emit('customEvent', { action: 'A' })}></button>;
}

猜你想问,事件和广播有什么区别?

简单来说,从结果看,它们能实现的功能基本上是相同的,都是一个组件调用,另外的组件执行相应操作。

区别在于“另外的组件”的数量,Broadcast会发往全局,任何一个注册了registerCallback的组件都会收到这个消息,如果你的消息是全局的这种就适合用Broadcast,其余情况下useEvent足以。

函数

对于提供方(注册函数)

import { useFunction } from '@/store/useFunction';
import { useEffect } from 'react';

export default function Provider() {
    const { registerFunction } = useFunction();

    useEffect(() => {
        registerFunction('greet', (name) => {
            return `Hello, ${name}!`;
        });

        registerFunction('sum', (a, b) => {
            return a + b;
        });
    }, [registerFunction]);

    return null;
}

对于调用方(调用已注册函数)

import { useFunction } from '@/store/useFunction';

export default function Caller() {
    const { callFunction } = useFunction();

    return (
        <div>
            <button onClick={() => console.log(callFunction('greet', 'Alice'))}>
                Say Hello
            </button>
            <button onClick={() => console.log(callFunction('sum', 5, 10))}>
                Add Numbers
            </button>
        </div>
    );
}

非常适合在组件间相互调用函数,让我回到了我以前写原生Js函数时的感觉。

(你还可以用hasFunction来检查是否已注册了这个函数)

代码

pnpm i zustand

useBroadcast

// @/store/useBroadcast.js
import { create } from 'zustand';

export const useBroadcast = create((set, get) => ({
    callbacks: [],

    registerCallback: (callback) => {
        set((state) => ({
            callbacks: [...state.callbacks, callback],
        }));
    },

    broadcast: (message) => {
        get().callbacks.forEach((callback) => callback(message));
    },

    unregisterCallback: (callback) => {
        set((state) => ({
            callbacks: state.callbacks.filter((cb) => cb !== callback),
        }));
    },
}));

useEvent

// @/store/useEvent.js
import { create } from 'zustand';

export const useEvent = create((set, get) => ({
    listeners: {},

    on: (eventName, listener) => {
        set((state) => ({
            listeners: {
                ...state.listeners,
                [eventName]: [...(state.listeners[eventName] || []), listener],
            },
        }));
    },

    emit: (eventName, ...args) => {
        const listeners = get().listeners[eventName] || [];
        listeners.forEach((listener) => listener(...args));
    },

    off: (eventName, listener) => {
        set((state) => ({
            listeners: {
                ...state.listeners,
                [eventName]: (state.listeners[eventName] || []).filter((l) => l !== listener),
            },
        }));
    },
}));

useFunction

// @/store/useFunction.js
import { create } from 'zustand';

export const useFunction = create((set, get) => ({
    functions: {},

    registerFunction: (name, fn) => {
        set((state) => ({
            functions: {
                ...state.functions,
                [name]: fn,
            },
        }));
    },

    callFunction: (name, ...args) => {
        const { functions } = get();
        const targetFn = functions[name];

        if (typeof targetFn === 'function') {
            return targetFn(...args);
        }
        return null;
    },

    hasFunction: (name) => {
        return typeof get().functions[name] === 'function';
    },
}));
INFO

框架状态


URL:
来源: ---
此页访问量:
此页访问人数:
此页平均滞留:
网络连接状态: 离线
Cookie状态: 已禁用
页面加载状态:
站点运行时长: ---

00:00


    无正在播放的音乐
    00:00/00:00


    账号
    User avatar
    未登录未设置描述...
    尚未登录,部分功能受限
    立刻 登录 注册
     未登录
    刷新进程离线
    当前未存储有效的TOKEN

    通知中心

    总计0条通知,0条未读通知
    当前未登录,无法查看通知


    - Mind stuff, that's what they say when the verses fly -