refactor(theme-common): unify missing context errors (#6826)

* refactor(theme-common): unify missing context errors

* update test

* more robust
This commit is contained in:
Joshua Chen 2022-03-03 22:26:56 +08:00 committed by GitHub
parent 5c60f41e1b
commit c387a177e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 35 additions and 28 deletions

View file

@ -120,6 +120,7 @@ export {
export { export {
useIsomorphicLayoutEffect, useIsomorphicLayoutEffect,
useDynamicCallback, useDynamicCallback,
ReactContextError,
} from './utils/reactUtils'; } from './utils/reactUtils';
export {isRegexpStringMatch} from './utils/regexpUtils'; export {isRegexpStringMatch} from './utils/regexpUtils';

View file

@ -71,7 +71,7 @@ describe('docsUtils', () => {
expect( expect(
() => renderHook(() => useDocsVersion()).result.current, () => renderHook(() => useDocsVersion()).result.current,
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"This hook requires usage of <DocsVersionProvider>"`, `"Hook useDocsVersion is called outside the <DocsVersionProvider>. "`,
); );
}); });
@ -93,7 +93,7 @@ describe('docsUtils', () => {
expect( expect(
() => renderHook(() => useDocsSidebar()).result.current, () => renderHook(() => useDocsSidebar()).result.current,
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"This hook requires usage of <DocsSidebarProvider>"`, `"Hook useDocsSidebar is called outside the <DocsSidebarProvider>. "`,
); );
}); });

View file

@ -16,6 +16,7 @@ import React, {
} from 'react'; } from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser'; import useIsBrowser from '@docusaurus/useIsBrowser';
import {createStorageSlot} from './storageUtils'; import {createStorageSlot} from './storageUtils';
import {ReactContextError} from './reactUtils';
import {useThemeConfig} from './useThemeConfig'; import {useThemeConfig} from './useThemeConfig';
export const AnnouncementBarDismissStorageKey = export const AnnouncementBarDismissStorageKey =
@ -110,12 +111,10 @@ export function AnnouncementBarProvider({
); );
} }
export const useAnnouncementBar = (): AnnouncementBarAPI => { export function useAnnouncementBar(): AnnouncementBarAPI {
const api = useContext(AnnouncementBarContext); const api = useContext(AnnouncementBarContext);
if (!api) { if (!api) {
throw new Error( throw new ReactContextError('AnnouncementBarProvider');
'useAnnouncementBar(): AnnouncementBar not found in React context: make sure to use the AnnouncementBarProvider on top of the tree',
);
} }
return api; return api;
}; }

View file

@ -5,14 +5,15 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {ReactNode} from 'react';
import React, { import React, {
useState, useState,
useCallback, useCallback,
useEffect, useEffect,
useContext, useContext,
useMemo, useMemo,
type ReactNode,
} from 'react'; } from 'react';
import {ReactContextError} from './reactUtils';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {createStorageSlot} from './storageUtils'; import {createStorageSlot} from './storageUtils';
@ -148,8 +149,9 @@ export function useColorMode(): ColorModeContextValue {
ColorModeContext, ColorModeContext,
); );
if (context == null) { if (context == null) {
throw new Error( throw new ReactContextError(
'"useColorMode()" is used outside of "Layout" component. Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.', 'ColorModeProvider',
'Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.',
); );
} }
return context; return context;

View file

@ -6,6 +6,7 @@
*/ */
import React, {type ReactNode, useMemo, useState, useContext} from 'react'; import React, {type ReactNode, useMemo, useState, useContext} from 'react';
import {ReactContextError} from './reactUtils';
const EmptyContext: unique symbol = Symbol('EmptyContext'); const EmptyContext: unique symbol = Symbol('EmptyContext');
const Context = React.createContext< const Context = React.createContext<
@ -33,9 +34,7 @@ export function DocSidebarItemsExpandedStateProvider({
export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState { export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState {
const contextValue = useContext(Context); const contextValue = useContext(Context);
if (contextValue === EmptyContext) { if (contextValue === EmptyContext) {
throw new Error( throw new ReactContextError('DocSidebarItemsExpandedStateProvider');
'This hook requires usage of <DocSidebarItemsExpandedStateProvider>',
);
} }
return contextValue; return contextValue;
} }

View file

@ -15,6 +15,7 @@ import React, {
} from 'react'; } from 'react';
import {useThemeConfig, type DocsVersionPersistence} from '../useThemeConfig'; import {useThemeConfig, type DocsVersionPersistence} from '../useThemeConfig';
import {isDocsPluginEnabled} from '../docsUtils'; import {isDocsPluginEnabled} from '../docsUtils';
import {ReactContextError} from '../reactUtils';
import { import {
useAllDocsData, useAllDocsData,
@ -159,9 +160,7 @@ function DocsPreferredVersionContextProviderUnsafe({
export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue { export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue {
const value = useContext(Context); const value = useContext(Context);
if (!value) { if (!value) {
throw new Error( throw new ReactContextError('DocsPreferredVersionContextProvider');
'Can\'t find docs preferred context, maybe you forgot to use the "DocsPreferredVersionContextProvider"?',
);
} }
return value; return value;
} }

View file

@ -19,6 +19,7 @@ import type {
PropSidebarBreadcrumbsItem, PropSidebarBreadcrumbsItem,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import {isSamePath} from './pathUtils'; import {isSamePath} from './pathUtils';
import {ReactContextError} from './reactUtils';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
// TODO not ideal, see also "useDocs" // TODO not ideal, see also "useDocs"
@ -49,7 +50,7 @@ export function DocsVersionProvider({
export function useDocsVersion(): PropVersionMetadata { export function useDocsVersion(): PropVersionMetadata {
const version = useContext(DocsVersionContext); const version = useContext(DocsVersionContext);
if (version === EmptyContextValue) { if (version === EmptyContextValue) {
throw new Error('This hook requires usage of <DocsVersionProvider>'); throw new ReactContextError('DocsVersionProvider');
} }
return version; return version;
} }
@ -89,7 +90,7 @@ export function DocsSidebarProvider({
export function useDocsSidebar(): PropSidebar | null { export function useDocsSidebar(): PropSidebar | null {
const sidebar = useContext(DocsSidebarContext); const sidebar = useContext(DocsSidebarContext);
if (sidebar === EmptyContextValue) { if (sidebar === EmptyContextValue) {
throw new Error('This hook requires usage of <DocsSidebarProvider>'); throw new ReactContextError('DocsSidebarProvider');
} }
return sidebar; return sidebar;
} }

View file

@ -14,6 +14,7 @@ import React, {
type ComponentType, type ComponentType,
useMemo, useMemo,
} from 'react'; } from 'react';
import {ReactContextError} from './reactUtils';
/* /*
The idea behind all this is that a specific component must be able to fill a 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 { function useMobileSecondaryMenuContext(): ContextValue {
const value = useContext(Context); const value = useContext(Context);
if (value === null) { if (value === null) {
throw new Error( throw new ReactContextError('MobileSecondaryMenuProvider');
'MobileSecondaryMenuProvider was not used correctly, context value is null',
);
} }
return value; return value;
} }

View file

@ -41,3 +41,13 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
// but good enough for our use // but good enough for our use
return useCallback<T>((...args) => ref.current(...args), []); return useCallback<T>((...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 (?<name>\w+)/)?.groups!.name
} is called outside the <${providerName}>. ${additionalInfo || ''}`;
}
}

View file

@ -15,7 +15,7 @@ import React, {
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import {useDynamicCallback} from './reactUtils'; import {useDynamicCallback, ReactContextError} from './reactUtils';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
/** /**
@ -76,9 +76,7 @@ export function ScrollControllerProvider({
export function useScrollController(): ScrollController { export function useScrollController(): ScrollController {
const context = useContext(ScrollMonitorContext); const context = useContext(ScrollMonitorContext);
if (context == null) { if (context == null) {
throw new Error( throw new ReactContextError('ScrollControllerProvider');
'"useScrollController" is used but no context provider was found in the React tree.',
);
} }
return context; return context;
} }

View file

@ -15,6 +15,7 @@ import React, {
type ReactNode, type ReactNode,
} from 'react'; } from 'react';
import {createStorageSlot, listStorageKeys} from './storageUtils'; import {createStorageSlot, listStorageKeys} from './storageUtils';
import {ReactContextError} from './reactUtils';
const TAB_CHOICE_PREFIX = 'docusaurus.tab.'; const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
@ -82,9 +83,7 @@ export function TabGroupChoiceProvider({
export function useTabGroupChoice(): TabGroupChoiceContextValue { export function useTabGroupChoice(): TabGroupChoiceContextValue {
const context = useContext(TabGroupChoiceContext); const context = useContext(TabGroupChoiceContext);
if (context == null) { if (context == null) {
throw new Error( throw new ReactContextError('TabGroupChoiceProvider');
'"useUserPreferencesContext" is used outside of "Layout" component.',
);
} }
return context; return context;
} }