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  }