mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-19 01:28:38 +02:00
refactor(theme-common): unify missing context errors (#6826)
* refactor(theme-common): unify missing context errors * update test * more robust
This commit is contained in:
parent
5c60f41e1b
commit
c387a177e8
11 changed files with 35 additions and 28 deletions
|
@ -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';
|
||||||
|
|
|
@ -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>. "`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 || ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue