MelonBlog

react使用provider实现一个简单的事件/信号系统

react中,两个组件之间交互可以通过传递状态的方式,但是如果两个组件没有显式的依赖关系,但是又需要交互,这时候可以通过provider 来传递状态。provider提供了一种全局状态机制,可以利用这种全局的机制做一个小事件/信号系统。

设计思路

事件/信号系统必须包含2个最核心的功能:

触发事件/信号
监听事件

除了触发事件之外呢,还可以增加一个辅助功能—重置信号,重置信号是有必要的,例如在某种场景下,会重复触发同一种信号,但是只需要处理一次,可以通过触重置信号来再次接收信号并处理。

通过一个初始化为0的state,让他的值更新为1 或者 当前时间戳,可以实现到底信号能触发多少次。通过useEffect来感知这个信号的变动,如果重复设置为1,useEffect是不会多次触发回调的。如果state设置成当前时间戳,则useEffect可以持续感知state的变化。

实现

provider.jsx

'use client'
import {createContext, useState} from 'react';
export const GlobalContext = createContext({});
const createSignal = (name) => {
    return {
        name,
        state: 0,
        data: null
    }
}
export function Providers({children}) {
    const [signals, setSignals] = useState({
        unauthorized: createSignal('unauthorized'), // 预定义信号,一些特殊场景需要在渲染完成之前就处理该信号
    });
    const listen = (signalName) => {
        console.log('监听信号: ', signalName)
        if (signals[signalName]) {
            return signals
        }
        const signal = createSignal(signalName)
        setSignals({
            ...signals,
            [signalName]: signal
        })
        return signals
    }
    const reset = (signal) => {
        const __signal = signals[signal.name]
        console.log('重置信号: ', __signal)
        setSignals({
            ...signals,
            [name]: {...__signal, state: 0, data: null},
        })
    }
    const emit = (signalName, data) => {
        const signal = signals[signalName]
        if (!signal) {
            console.warn('信号不存在: ', signalName)
            return
        }
        signal.state = 1
        signal.data = data
        console.log('触发信号: ', signal)
        setSignals({
            ...signals,
            [signalName]: {...signal},
        })
    }
    return (
        <GlobalContext.Provider value={{listen, emit, reset}}>
                {children}
        </GlobalContext.Provider>
    )
}

上面的代码实现了一个事件机制,通过这个provider,业务组件可以使用 listen函数来监听某一种信号或者事件,并通过useEffect回调来感知信号的变动,使用reset函数可以重置某一个信号,使用emit函数可以触发某一个信号,如果信号不存在,则会新建信号。

例子🌰

需求:每次请求我都会监听请求是否401,如果401则为token失效,如果token失效,则要跳转到登录页


通过一个包装过的fetch来处理401请求,遇到401之后emit一个unauthorized信号

function __fetch__(ctx, url, options) {
    return fetch(url, options).then(response => {
        console.log('response: ', response)
        if (response.status !== 200) {
            console.error('请求失败:', response.statusText)
            return {code: -1, message: '请求失败'}
        }
        return response.json()
    }).then(data => {
        return data;
    }).catch(e => {
        // 401 会执行到这里
        console.error('请求异常:', e)
        ctx.emit('unauthorized', e)
        return {code: -1, message: '请求异常'}
    })
}

用一个Auth组件持续监听token是否失效信号,遇到unauthorized信号则跳转到login页

'use client'
import {useContext, useEffect} from "react";
import {GlobalContext} from "@/app/providers";
import {useRouter} from "next/navigation";
function Auth() {
    const ctx = useContext(GlobalContext)
    const router = useRouter()
    const signals = ctx.listen('unauthorized')
    console.log('signals: ', signals)
    useEffect(() => {
        const signal = signals['unauthorized']
        console.log('收到信号: ', signal)
        if (signal && signal.state) {
            console.log('收到信号[unauthorized]')
            ctx.reset(signal)
            localStorage.removeItem('token')
            router.push('/login')
        } else {
            console.log('忽略信号')
        }
    }, [signals['unauthorized']])
    return (<>
    </>);
}