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 {
useIsomorphicLayoutEffect,
useDynamicCallback,
ReactContextError,
} from './utils/reactUtils';
export {isRegexpStringMatch} from './utils/regexpUtils';

View file

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

View file

@ -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;
};
}

View file

@ -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;

View file

@ -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 <DocSidebarItemsExpandedStateProvider>',
);
throw new ReactContextError('DocSidebarItemsExpandedStateProvider');
}
return contextValue;
}

View file

@ -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;
}

View file

@ -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 <DocsVersionProvider>');
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 <DocsSidebarProvider>');
throw new ReactContextError('DocsSidebarProvider');
}
return sidebar;
}

View file

@ -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;
}

View file

@ -41,3 +41,13 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
// but good enough for our use
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,
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;
}

View file

@ -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;
}