go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/generic_libs/components/routed_tabs/context.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Dispatch, createContext, useContext, useEffect, useRef } from 'react'; 16 17 import { Action } from './reducer'; 18 19 export interface ActiveTabContextValue { 20 readonly activeTabId: string | null; 21 } 22 23 const ActiveTabContext = createContext<ActiveTabContextValue | null>(null); 24 25 export const ActiveTabContextProvider = ActiveTabContext.Provider; 26 27 /** 28 * Get the tab ID of the active tab. 29 */ 30 export function useActiveTabId() { 31 const ctx = useContext(ActiveTabContext); 32 33 if (!ctx) { 34 throw new Error('useActiveTabId must be used within RoutedTabs'); 35 } 36 37 return ctx.activeTabId; 38 } 39 40 // Keep the dispatch in a separate context so updating the active tab doesn't 41 // trigger refresh on components that only consume the dispatch action (which 42 // is rarely updated if at all). 43 const ActiveTabUpdaterContext = createContext<Dispatch<Action> | null>(null); 44 45 export const ActiveTabUpdaterContextProvider = ActiveTabUpdaterContext.Provider; 46 47 /** 48 * Mark the component with a tab ID. When the component is mounted, marked the 49 * tab ID as activated. 50 * 51 * For each `<RoutedTabs />`, at most one tab can be activated at a time. 52 */ 53 export function useTabId(id: string) { 54 const hookRef = useRef(); 55 const dispatch = useContext(ActiveTabUpdaterContext); 56 if (!dispatch) { 57 throw new Error('useTabId must be used within RoutedTabs'); 58 } 59 60 useEffect(() => { 61 dispatch({ type: 'activateTab', id, hookRef }); 62 }, [dispatch, id]); 63 64 // Wrap id in a ref so we don't need to declare it as a dependency. 65 // We only need to deactivate tab when `dispatch` is changed (i.e. the 66 // parent context is being switched), or when the component is being 67 // unmounted. 68 const latestIdRef = useRef(id); 69 latestIdRef.current = id; 70 useEffect( 71 () => () => 72 dispatch({ type: 'deactivateTab', id: latestIdRef.current, hookRef }), 73 [dispatch], 74 ); 75 }