From c387a177e8601013fcebab678f5e7d933e06c0f1 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 3 Mar 2022 22:26:56 +0800 Subject: [PATCH] refactor(theme-common): unify missing context errors (#6826) * refactor(theme-common): unify missing context errors * update test * more robust --- packages/docusaurus-theme-common/src/index.ts | 1 + .../src/utils/__tests__/docsUtils.test.tsx | 4 ++-- .../src/utils/announcementBarUtils.tsx | 9 ++++----- .../src/utils/colorModeUtils.tsx | 8 +++++--- .../src/utils/docSidebarItemsExpandedState.tsx | 5 ++--- .../DocsPreferredVersionProvider.tsx | 5 ++--- .../docusaurus-theme-common/src/utils/docsUtils.tsx | 5 +++-- .../src/utils/mobileSecondaryMenu.tsx | 5 ++--- .../docusaurus-theme-common/src/utils/reactUtils.tsx | 10 ++++++++++ .../docusaurus-theme-common/src/utils/scrollUtils.tsx | 6 ++---- .../src/utils/tabGroupChoiceUtils.tsx | 5 ++--- 11 files changed, 35 insertions(+), 28 deletions(-) diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 2e0cbbe9ea..c1138eec25 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -120,6 +120,7 @@ export { export { useIsomorphicLayoutEffect, useDynamicCallback, + ReactContextError, } from './utils/reactUtils'; export {isRegexpStringMatch} from './utils/regexpUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx index e0700f8d86..b08b6862bf 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx @@ -71,7 +71,7 @@ describe('docsUtils', () => { expect( () => renderHook(() => useDocsVersion()).result.current, ).toThrowErrorMatchingInlineSnapshot( - `"This hook requires usage of "`, + `"Hook useDocsVersion is called outside the . "`, ); }); @@ -93,7 +93,7 @@ describe('docsUtils', () => { expect( () => renderHook(() => useDocsSidebar()).result.current, ).toThrowErrorMatchingInlineSnapshot( - `"This hook requires usage of "`, + `"Hook useDocsSidebar is called outside the . "`, ); }); diff --git a/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx b/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx index 81067f6df3..0983cca167 100644 --- a/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx @@ -16,6 +16,7 @@ import React, { } from 'react'; import useIsBrowser from '@docusaurus/useIsBrowser'; import {createStorageSlot} from './storageUtils'; +import {ReactContextError} from './reactUtils'; import {useThemeConfig} from './useThemeConfig'; export const AnnouncementBarDismissStorageKey = @@ -110,12 +111,10 @@ export function AnnouncementBarProvider({ ); } -export const useAnnouncementBar = (): AnnouncementBarAPI => { +export function useAnnouncementBar(): AnnouncementBarAPI { const api = useContext(AnnouncementBarContext); if (!api) { - throw new Error( - 'useAnnouncementBar(): AnnouncementBar not found in React context: make sure to use the AnnouncementBarProvider on top of the tree', - ); + throw new ReactContextError('AnnouncementBarProvider'); } return api; -}; +} diff --git a/packages/docusaurus-theme-common/src/utils/colorModeUtils.tsx b/packages/docusaurus-theme-common/src/utils/colorModeUtils.tsx index 5167d82026..c955206c1a 100644 --- a/packages/docusaurus-theme-common/src/utils/colorModeUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/colorModeUtils.tsx @@ -5,14 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -import type {ReactNode} from 'react'; import React, { useState, useCallback, useEffect, useContext, useMemo, + type ReactNode, } from 'react'; +import {ReactContextError} from './reactUtils'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import {createStorageSlot} from './storageUtils'; @@ -148,8 +149,9 @@ export function useColorMode(): ColorModeContextValue { ColorModeContext, ); if (context == null) { - throw new Error( - '"useColorMode()" is used outside of "Layout" component. Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.', + throw new ReactContextError( + 'ColorModeProvider', + 'Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.', ); } return context; diff --git a/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx b/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx index c2435f6931..e3f040db6c 100644 --- a/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx +++ b/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx @@ -6,6 +6,7 @@ */ import React, {type ReactNode, useMemo, useState, useContext} from 'react'; +import {ReactContextError} from './reactUtils'; const EmptyContext: unique symbol = Symbol('EmptyContext'); const Context = React.createContext< @@ -33,9 +34,7 @@ export function DocSidebarItemsExpandedStateProvider({ export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState { const contextValue = useContext(Context); if (contextValue === EmptyContext) { - throw new Error( - 'This hook requires usage of ', - ); + throw new ReactContextError('DocSidebarItemsExpandedStateProvider'); } return contextValue; } diff --git a/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx b/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx index edc98ca1b3..dd940cb25a 100644 --- a/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx @@ -15,6 +15,7 @@ import React, { } from 'react'; import {useThemeConfig, type DocsVersionPersistence} from '../useThemeConfig'; import {isDocsPluginEnabled} from '../docsUtils'; +import {ReactContextError} from '../reactUtils'; import { useAllDocsData, @@ -159,9 +160,7 @@ function DocsPreferredVersionContextProviderUnsafe({ export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue { const value = useContext(Context); if (!value) { - throw new Error( - 'Can\'t find docs preferred context, maybe you forgot to use the "DocsPreferredVersionContextProvider"?', - ); + throw new ReactContextError('DocsPreferredVersionContextProvider'); } return value; } diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index f02b213c32..be21b456b3 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -19,6 +19,7 @@ import type { PropSidebarBreadcrumbsItem, } from '@docusaurus/plugin-content-docs'; import {isSamePath} from './pathUtils'; +import {ReactContextError} from './reactUtils'; import {useLocation} from '@docusaurus/router'; // TODO not ideal, see also "useDocs" @@ -49,7 +50,7 @@ export function DocsVersionProvider({ export function useDocsVersion(): PropVersionMetadata { const version = useContext(DocsVersionContext); if (version === EmptyContextValue) { - throw new Error('This hook requires usage of '); + throw new ReactContextError('DocsVersionProvider'); } return version; } @@ -89,7 +90,7 @@ export function DocsSidebarProvider({ export function useDocsSidebar(): PropSidebar | null { const sidebar = useContext(DocsSidebarContext); if (sidebar === EmptyContextValue) { - throw new Error('This hook requires usage of '); + throw new ReactContextError('DocsSidebarProvider'); } return sidebar; } diff --git a/packages/docusaurus-theme-common/src/utils/mobileSecondaryMenu.tsx b/packages/docusaurus-theme-common/src/utils/mobileSecondaryMenu.tsx index 74bead0bd0..94cccc2aed 100644 --- a/packages/docusaurus-theme-common/src/utils/mobileSecondaryMenu.tsx +++ b/packages/docusaurus-theme-common/src/utils/mobileSecondaryMenu.tsx @@ -14,6 +14,7 @@ import React, { type ComponentType, useMemo, } from 'react'; +import {ReactContextError} from './reactUtils'; /* The idea behind all this is that a specific component must be able to fill a @@ -60,9 +61,7 @@ export function MobileSecondaryMenuProvider({ function useMobileSecondaryMenuContext(): ContextValue { const value = useContext(Context); if (value === null) { - throw new Error( - 'MobileSecondaryMenuProvider was not used correctly, context value is null', - ); + throw new ReactContextError('MobileSecondaryMenuProvider'); } return value; } diff --git a/packages/docusaurus-theme-common/src/utils/reactUtils.tsx b/packages/docusaurus-theme-common/src/utils/reactUtils.tsx index e934235c69..ae34933d1b 100644 --- a/packages/docusaurus-theme-common/src/utils/reactUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/reactUtils.tsx @@ -41,3 +41,13 @@ export function useDynamicCallback unknown>( // but good enough for our use return useCallback((...args) => ref.current(...args), []); } + +export class ReactContextError extends Error { + constructor(providerName: string, additionalInfo?: string) { + super(); + this.name = 'ReactContextError'; + this.message = `Hook ${ + this.stack?.split('\n')[1]?.match(/at (?\w+)/)?.groups!.name + } is called outside the <${providerName}>. ${additionalInfo || ''}`; + } +} diff --git a/packages/docusaurus-theme-common/src/utils/scrollUtils.tsx b/packages/docusaurus-theme-common/src/utils/scrollUtils.tsx index 68e8cfc581..7c4fa30033 100644 --- a/packages/docusaurus-theme-common/src/utils/scrollUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/scrollUtils.tsx @@ -15,7 +15,7 @@ import React, { useMemo, useRef, } from 'react'; -import {useDynamicCallback} from './reactUtils'; +import {useDynamicCallback, ReactContextError} from './reactUtils'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; /** @@ -76,9 +76,7 @@ export function ScrollControllerProvider({ export function useScrollController(): ScrollController { const context = useContext(ScrollMonitorContext); if (context == null) { - throw new Error( - '"useScrollController" is used but no context provider was found in the React tree.', - ); + throw new ReactContextError('ScrollControllerProvider'); } return context; } diff --git a/packages/docusaurus-theme-common/src/utils/tabGroupChoiceUtils.tsx b/packages/docusaurus-theme-common/src/utils/tabGroupChoiceUtils.tsx index e11df2de18..4348a74a98 100644 --- a/packages/docusaurus-theme-common/src/utils/tabGroupChoiceUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/tabGroupChoiceUtils.tsx @@ -15,6 +15,7 @@ import React, { type ReactNode, } from 'react'; import {createStorageSlot, listStorageKeys} from './storageUtils'; +import {ReactContextError} from './reactUtils'; const TAB_CHOICE_PREFIX = 'docusaurus.tab.'; @@ -82,9 +83,7 @@ export function TabGroupChoiceProvider({ export function useTabGroupChoice(): TabGroupChoiceContextValue { const context = useContext(TabGroupChoiceContext); if (context == null) { - throw new Error( - '"useUserPreferencesContext" is used outside of "Layout" component.', - ); + throw new ReactContextError('TabGroupChoiceProvider'); } return context; }