From 9e1c0d8b958a048db7fdbb297c24ed3c2dc25e54 Mon Sep 17 00:00:00 2001 From: memoyil <2213635+memoyil@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:45:52 +0200 Subject: [PATCH] perf: Make history manager provider render only with subscribe --- apps/web/src/contexts/HistoryContext.tsx | 72 ++++++++++++++++++++---- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/apps/web/src/contexts/HistoryContext.tsx b/apps/web/src/contexts/HistoryContext.tsx index 485b965e860a0b..d76e9902b9de39 100644 --- a/apps/web/src/contexts/HistoryContext.tsx +++ b/apps/web/src/contexts/HistoryContext.tsx @@ -1,28 +1,66 @@ import { useRouter } from 'next/router' -import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' -const historyManagerContext = createContext | null>(null) +interface HistoryManagerContext { + history: string[] + canGoBack: () => boolean + subscribe: (subscriber: Subscriber) => void + unsubscribe: (subscriber: Subscriber) => void +} + +interface Subscriber { + setState: React.Dispatch> +} -export function HistoryManagerProvider({ children }) { +const historyManagerContext = createContext(null) + +export function HistoryManagerProvider({ children }: { children: React.ReactNode }) { const value = useHistoryManager() return {children} } -export const useHistory = () => useContext(historyManagerContext) +export const useHistory = () => { + const context = useContext(historyManagerContext) + if (!context) { + throw new Error('useHistory must be used within a HistoryManagerProvider') + } + + const [, setState] = useState() + + useEffect(() => { + const subscriber = { setState } + context.subscribe(subscriber) + + return () => { + context.unsubscribe(subscriber) + } + }, [context]) -function useHistoryManager() { + return context +} + +function useHistoryManager(): HistoryManagerContext { const router = useRouter() const [history, setHistory] = useState(() => [router?.asPath]) + const subscribersRef = useRef([]) useEffect(() => { - const handleRouteChange = (url, { shallow }) => { + const handleRouteChange = (url: string, { shallow }: { shallow: boolean }) => { if (!shallow) { - setHistory((prevState) => [...prevState, url]) + setHistory((prevState) => { + const newHistory = [...prevState, url] + notifySubscribers(newHistory) + return newHistory + }) } } router.beforePopState(() => { - setHistory((prevState) => prevState.slice(0, -2)) + setHistory((prevState) => { + const newHistory = prevState.slice(0, -2) + notifySubscribers(newHistory) + return newHistory + }) return true }) @@ -34,7 +72,21 @@ function useHistoryManager() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const notifySubscribers = useCallback((newHistory: string[]) => { + subscribersRef.current.forEach((subscriber) => { + subscriber.setState(newHistory) + }) + }, []) + + const subscribe = useCallback((subscriber: Subscriber) => { + subscribersRef.current.push(subscriber) + }, []) + + const unsubscribe = useCallback((subscriber: Subscriber) => { + subscribersRef.current = subscribersRef.current.filter((sub) => sub !== subscriber) + }, []) + return useMemo(() => { - return { history, canGoBack: () => history.length > 1 } - }, [history]) + return { history, canGoBack: () => history.length > 1, subscribe, unsubscribe } + }, [history, subscribe, unsubscribe]) }