mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-16 17:52:29 +02:00
refactor(core): replace useDocusaurusContext().isClient by useIsBrowser() (#5349)
* extract separate useIsClient() hook * for consistency, rename to `useIsBrowser` * useless return * improve doc for BrowserOnly * update snapshot * polish
This commit is contained in:
parent
69b11a8546
commit
295e77cc09
20 changed files with 213 additions and 90 deletions
|
@ -179,6 +179,10 @@ declare module '@docusaurus/useDocusaurusContext' {
|
||||||
export default function useDocusaurusContext(): DocusaurusContext;
|
export default function useDocusaurusContext(): DocusaurusContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@docusaurus/useIsBrowser' {
|
||||||
|
export default function useIsBrowser(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@docusaurus/useBaseUrl' {
|
declare module '@docusaurus/useBaseUrl' {
|
||||||
export type BaseUrlOptions = {
|
export type BaseUrlOptions = {
|
||||||
forcePrependBaseUrl?: boolean;
|
forcePrependBaseUrl?: boolean;
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import useThemeContext from '@theme/hooks/useThemeContext';
|
import useThemeContext from '@theme/hooks/useThemeContext';
|
||||||
import type {Props} from '@theme/ThemedImage';
|
import type {Props} from '@theme/ThemedImage';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
const ThemedImage = (props: Props): JSX.Element => {
|
const ThemedImage = (props: Props): JSX.Element => {
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
const {isDarkTheme} = useThemeContext();
|
const {isDarkTheme} = useThemeContext();
|
||||||
const {sources, className, alt = '', ...propsRest} = props;
|
const {sources, className, alt = '', ...propsRest} = props;
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ const ThemedImage = (props: Props): JSX.Element => {
|
||||||
|
|
||||||
const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light'];
|
const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light'];
|
||||||
|
|
||||||
const renderedSourceNames: SourceName[] = isClient
|
const renderedSourceNames: SourceName[] = isBrowser
|
||||||
? clientThemes
|
? clientThemes
|
||||||
: // We need to render both images on the server to avoid flash
|
: // We need to render both images on the server to avoid flash
|
||||||
// See https://github.com/facebook/docusaurus/pull/3730
|
// See https://github.com/facebook/docusaurus/pull/3730
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import React, {ReactNode, useState, useCallback} from 'react';
|
import React, {ReactNode, useState, useCallback} from 'react';
|
||||||
import {MDXProvider} from '@mdx-js/react';
|
import {MDXProvider} from '@mdx-js/react';
|
||||||
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
||||||
import renderRoutes from '@docusaurus/renderRoutes';
|
import renderRoutes from '@docusaurus/renderRoutes';
|
||||||
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types';
|
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
|
@ -37,7 +36,6 @@ function DocPageContent({
|
||||||
versionMetadata,
|
versionMetadata,
|
||||||
children,
|
children,
|
||||||
}: DocPageContentProps): JSX.Element {
|
}: DocPageContentProps): JSX.Element {
|
||||||
const {isClient} = useDocusaurusContext();
|
|
||||||
const {pluginId, version} = versionMetadata;
|
const {pluginId, version} = versionMetadata;
|
||||||
|
|
||||||
const sidebarName = currentDocRoute.sidebar;
|
const sidebarName = currentDocRoute.sidebar;
|
||||||
|
@ -57,7 +55,6 @@ function DocPageContent({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
key={`${isClient}`} // TODO seems suspicious
|
|
||||||
wrapperClassName={ThemeClassNames.wrapper.docPages}
|
wrapperClassName={ThemeClassNames.wrapper.docPages}
|
||||||
pageClassName={ThemeClassNames.page.docPage}
|
pageClassName={ThemeClassNames.page.docPage}
|
||||||
searchMetadatas={{
|
searchMetadatas={{
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {useThemeConfig} from '@docusaurus/theme-common';
|
||||||
const Logo = (props: Props): JSX.Element => {
|
const Logo = (props: Props): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
siteConfig: {title},
|
siteConfig: {title},
|
||||||
isClient,
|
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
const {
|
const {
|
||||||
navbar: {title: navbarTitle, logo = {src: ''}},
|
navbar: {title: navbarTitle, logo = {src: ''}},
|
||||||
|
@ -37,7 +36,6 @@ const Logo = (props: Props): JSX.Element => {
|
||||||
{...(logo.target && {target: logo.target})}>
|
{...(logo.target && {target: logo.target})}>
|
||||||
{logo.src && (
|
{logo.src && (
|
||||||
<ThemedImage
|
<ThemedImage
|
||||||
key={`${isClient}`} // TODO seems suspicious
|
|
||||||
className={imageClassName}
|
className={imageClassName}
|
||||||
sources={sources}
|
sources={sources}
|
||||||
alt={logo.alt || navbarTitle || title}
|
alt={logo.alt || navbarTitle || title}
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import useThemeContext from '@theme/hooks/useThemeContext';
|
import useThemeContext from '@theme/hooks/useThemeContext';
|
||||||
import type {Props} from '@theme/ThemedImage';
|
import type {Props} from '@theme/ThemedImage';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
const ThemedImage = (props: Props): JSX.Element => {
|
const ThemedImage = (props: Props): JSX.Element => {
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
const {isDarkTheme} = useThemeContext();
|
const {isDarkTheme} = useThemeContext();
|
||||||
const {sources, className, alt = '', ...propsRest} = props;
|
const {sources, className, alt = '', ...propsRest} = props;
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ const ThemedImage = (props: Props): JSX.Element => {
|
||||||
|
|
||||||
const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light'];
|
const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light'];
|
||||||
|
|
||||||
const renderedSourceNames: SourceName[] = isClient
|
const renderedSourceNames: SourceName[] = isBrowser
|
||||||
? clientThemes
|
? clientThemes
|
||||||
: // We need to render both images on the server to avoid flash
|
: // We need to render both images on the server to avoid flash
|
||||||
// See https://github.com/facebook/docusaurus/pull/3730
|
// See https://github.com/facebook/docusaurus/pull/3730
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import React, {useState, useRef, memo, CSSProperties} from 'react';
|
import React, {useState, useRef, memo, CSSProperties} from 'react';
|
||||||
import type {Props} from '@theme/Toggle';
|
import type {Props} from '@theme/Toggle';
|
||||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
@ -91,11 +91,11 @@ export default function (props: Props): JSX.Element {
|
||||||
switchConfig: {darkIcon, darkIconStyle, lightIcon, lightIconStyle},
|
switchConfig: {darkIcon, darkIconStyle, lightIcon, lightIconStyle},
|
||||||
},
|
},
|
||||||
} = useThemeConfig();
|
} = useThemeConfig();
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toggle
|
<Toggle
|
||||||
disabled={!isClient}
|
disabled={!isBrowser}
|
||||||
icons={{
|
icons={{
|
||||||
checked: <Dark icon={darkIcon} style={darkIconStyle} />,
|
checked: <Dark icon={darkIcon} style={darkIconStyle} />,
|
||||||
unchecked: <Light icon={lightIcon} style={lightIconStyle} />,
|
unchecked: <Light icon={lightIcon} style={lightIconStyle} />,
|
||||||
|
|
|
@ -47,10 +47,6 @@ function useWindowSize(): WindowSize {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ExecutionEnvironment.canUseDOM) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWindowSize() {
|
function updateWindowSize() {
|
||||||
setWindowSize(getWindowSize());
|
setWindowSize(getWindowSize());
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ComponentProps, ReactElement, useRef, useState} from 'react';
|
import React, {ComponentProps, ReactElement, useRef, useState} from 'react';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {useCollapsible, Collapsible} from '../Collapsible';
|
import {useCollapsible, Collapsible} from '../Collapsible';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
@ -30,7 +30,7 @@ export type DetailsProps = {
|
||||||
} & ComponentProps<'details'>;
|
} & ComponentProps<'details'>;
|
||||||
|
|
||||||
const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => {
|
const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => {
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||||
|
|
||||||
const {collapsed, setCollapsed} = useCollapsible({
|
const {collapsed, setCollapsed} = useCollapsible({
|
||||||
|
@ -48,7 +48,7 @@ const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => {
|
||||||
data-collapsed={collapsed}
|
data-collapsed={collapsed}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.details,
|
styles.details,
|
||||||
{[styles.isClient]: isClient},
|
{[styles.isBrowser]: isBrowser},
|
||||||
props.className,
|
props.className,
|
||||||
)}>
|
)}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||||
|
|
|
@ -46,9 +46,9 @@ CSS variables, meant to be overriden by final theme
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When JS disabled/failed to load: we use the open property for arrow animation: */
|
/* When JS disabled/failed to load: we use the open property for arrow animation: */
|
||||||
.details[open]:not(.isClient) > summary:before,
|
.details[open]:not(.isBrowser) > summary:before,
|
||||||
/* When JS works: we use the data-attribute for arrow animation */
|
/* When JS works: we use the data-attribute for arrow animation */
|
||||||
.details[data-collapsed='false'].isClient > summary:before {
|
.details[data-collapsed='false'].isBrowser > summary:before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import React, {
|
||||||
useContext,
|
useContext,
|
||||||
createContext,
|
createContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import {createStorageSlot} from './storageUtils';
|
import {createStorageSlot} from './storageUtils';
|
||||||
import {useThemeConfig} from './useThemeConfig';
|
import {useThemeConfig} from './useThemeConfig';
|
||||||
|
|
||||||
|
@ -39,10 +39,10 @@ type AnnouncementBarAPI = {
|
||||||
|
|
||||||
const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
|
const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
|
||||||
const {announcementBar} = useThemeConfig();
|
const {announcementBar} = useThemeConfig();
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
|
|
||||||
const [isClosed, setClosed] = useState(() => {
|
const [isClosed, setClosed] = useState(() => {
|
||||||
return isClient
|
return isBrowser
|
||||||
? // On client navigation: init with localstorage value
|
? // On client navigation: init with localstorage value
|
||||||
isDismissedInStorage()
|
isDismissedInStorage()
|
||||||
: // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed)
|
: // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Translate from '@docusaurus/Translate';
|
import Translate from '@docusaurus/Translate';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import usePrismTheme from '@theme/hooks/usePrismTheme';
|
import usePrismTheme from '@theme/hooks/usePrismTheme';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
@ -51,8 +52,8 @@ function EditorWithHeader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Playground({children, transformCode, ...props}) {
|
export default function Playground({children, transformCode, ...props}) {
|
||||||
|
const isBrowser = useIsBrowser();
|
||||||
const {
|
const {
|
||||||
isClient,
|
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
liveCodeBlock: {playgroundPosition},
|
liveCodeBlock: {playgroundPosition},
|
||||||
|
@ -64,8 +65,8 @@ export default function Playground({children, transformCode, ...props}) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.playgroundContainer}>
|
<div className={styles.playgroundContainer}>
|
||||||
<LiveProvider
|
<LiveProvider
|
||||||
key={isClient}
|
key={isBrowser}
|
||||||
code={isClient ? children.replace(/\n$/, '') : ''}
|
code={isBrowser ? children.replace(/\n$/, '') : ''}
|
||||||
transformCode={transformCode || ((code) => `${code};`)}
|
transformCode={transformCode || ((code) => `${code};`)}
|
||||||
theme={prismTheme}
|
theme={prismTheme}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
|
5
packages/docusaurus-types/src/index.d.ts
vendored
5
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -125,7 +125,10 @@ export interface DocusaurusContext {
|
||||||
globalData: Record<string, unknown>;
|
globalData: Record<string, unknown>;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
codeTranslations: Record<string, string>;
|
codeTranslations: Record<string, string>;
|
||||||
isClient: boolean;
|
|
||||||
|
// Don't put mutable values here, to avoid triggering re-renders
|
||||||
|
// We could reconsider that choice if context selectors are implemented
|
||||||
|
// isBrowser: boolean; // Not here on purpose!
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
|
|
|
@ -5,16 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import routes from '@generated/routes';
|
import routes from '@generated/routes';
|
||||||
import siteConfig from '@generated/docusaurus.config';
|
|
||||||
import globalData from '@generated/globalData';
|
|
||||||
import i18n from '@generated/i18n';
|
|
||||||
import codeTranslations from '@generated/codeTranslations';
|
|
||||||
import siteMetadata from '@generated/site-metadata';
|
|
||||||
import renderRoutes from './exports/renderRoutes';
|
import renderRoutes from './exports/renderRoutes';
|
||||||
import DocusaurusContext from './exports/context';
|
import {BrowserContextProvider} from './exports/browserContext';
|
||||||
|
import {DocusaurusContextProvider} from './exports/docusaurusContext';
|
||||||
import PendingNavigation from './PendingNavigation';
|
import PendingNavigation from './PendingNavigation';
|
||||||
import BaseUrlIssueBanner from './baseUrlIssueBanner/BaseUrlIssueBanner';
|
import BaseUrlIssueBanner from './baseUrlIssueBanner/BaseUrlIssueBanner';
|
||||||
import Root from '@theme/Root';
|
import Root from '@theme/Root';
|
||||||
|
@ -22,29 +18,17 @@ import Root from '@theme/Root';
|
||||||
import './client-lifecycles-dispatcher';
|
import './client-lifecycles-dispatcher';
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const [isClient, setIsClient] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsClient(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocusaurusContext.Provider
|
<DocusaurusContextProvider>
|
||||||
value={{
|
<BrowserContextProvider>
|
||||||
siteConfig,
|
|
||||||
siteMetadata,
|
|
||||||
globalData,
|
|
||||||
i18n,
|
|
||||||
codeTranslations,
|
|
||||||
isClient,
|
|
||||||
}}>
|
|
||||||
<Root>
|
<Root>
|
||||||
<BaseUrlIssueBanner />
|
<BaseUrlIssueBanner />
|
||||||
<PendingNavigation routes={routes}>
|
<PendingNavigation routes={routes}>
|
||||||
{renderRoutes(routes)}
|
{renderRoutes(routes)}
|
||||||
</PendingNavigation>
|
</PendingNavigation>
|
||||||
</Root>
|
</Root>
|
||||||
</DocusaurusContext.Provider>
|
</BrowserContextProvider>
|
||||||
|
</DocusaurusContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
|
|
||||||
|
// Similar comp to the one described here:
|
||||||
|
// https://www.joshwcomeau.com/react/the-perils-of-rehydration/#abstractions
|
||||||
function BrowserOnly({
|
function BrowserOnly({
|
||||||
children,
|
children,
|
||||||
fallback,
|
fallback,
|
||||||
|
@ -15,9 +17,9 @@ function BrowserOnly({
|
||||||
children?: () => JSX.Element;
|
children?: () => JSX.Element;
|
||||||
fallback?: JSX.Element;
|
fallback?: JSX.Element;
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const {isClient} = useDocusaurusContext();
|
const isBrowser = useIsBrowser();
|
||||||
|
|
||||||
if (isClient && children != null) {
|
if (isBrowser && children != null) {
|
||||||
return <>{children()}</>;
|
return <>{children()}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
packages/docusaurus/src/client/exports/browserContext.tsx
Normal file
32
packages/docusaurus/src/client/exports/browserContext.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {ReactNode, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
// Encapsulate the logic to avoid React hydration problems
|
||||||
|
// See https://www.joshwcomeau.com/react/the-perils-of-rehydration/
|
||||||
|
// On first client-side render, we need to render exactly as the server rendered
|
||||||
|
// isBrowser is set to true only after a successful hydration
|
||||||
|
|
||||||
|
// Note, isBrowser is not part of useDocusaurusContext() for perf reasons
|
||||||
|
// Using useDocusaurusContext() (much more common need) should not trigger re-rendering after a successful hydration
|
||||||
|
|
||||||
|
export const Context = React.createContext<boolean>(false);
|
||||||
|
|
||||||
|
export function BrowserContextProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [isBrowser, setIsBrowser] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsBrowser(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Context.Provider value={isBrowser}>{children}</Context.Provider>;
|
||||||
|
}
|
35
packages/docusaurus/src/client/exports/docusaurusContext.tsx
Normal file
35
packages/docusaurus/src/client/exports/docusaurusContext.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {ReactNode} from 'react';
|
||||||
|
import {DocusaurusContext} from '@docusaurus/types';
|
||||||
|
|
||||||
|
import siteConfig from '@generated/docusaurus.config';
|
||||||
|
import globalData from '@generated/globalData';
|
||||||
|
import i18n from '@generated/i18n';
|
||||||
|
import codeTranslations from '@generated/codeTranslations';
|
||||||
|
import siteMetadata from '@generated/site-metadata';
|
||||||
|
|
||||||
|
// Static value on purpose: don't make it dynamic!
|
||||||
|
// Using context is still useful for testability reasons.
|
||||||
|
const contextValue: DocusaurusContext = {
|
||||||
|
siteConfig,
|
||||||
|
siteMetadata,
|
||||||
|
globalData,
|
||||||
|
i18n,
|
||||||
|
codeTranslations,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Context = React.createContext<DocusaurusContext>(contextValue);
|
||||||
|
|
||||||
|
export function DocusaurusContextProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||||
|
}
|
|
@ -6,16 +6,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useContext} from 'react';
|
import {useContext} from 'react';
|
||||||
import context from './context';
|
import {Context} from './docusaurusContext';
|
||||||
import {DocusaurusContext} from '@docusaurus/types';
|
import {DocusaurusContext} from '@docusaurus/types';
|
||||||
|
|
||||||
function useDocusaurusContext(): DocusaurusContext {
|
function useDocusaurusContext(): DocusaurusContext {
|
||||||
const docusaurusContext = useContext(context);
|
return useContext(Context);
|
||||||
if (docusaurusContext === null) {
|
|
||||||
// should not happen normally
|
|
||||||
throw new Error('Docusaurus context not provided.');
|
|
||||||
}
|
|
||||||
return docusaurusContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useDocusaurusContext;
|
export default useDocusaurusContext;
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import {useContext} from 'react';
|
||||||
import {DocusaurusContext} from '@docusaurus/types';
|
import {Context} from './browserContext';
|
||||||
|
|
||||||
export default React.createContext<DocusaurusContext | null>(null);
|
export default function useIsBrowser(): boolean {
|
||||||
|
return useContext(Context);
|
||||||
|
}
|
|
@ -10,14 +10,16 @@ Object {
|
||||||
"@docusaurus/Link": "../../../../client/exports/Link.tsx",
|
"@docusaurus/Link": "../../../../client/exports/Link.tsx",
|
||||||
"@docusaurus/Noop": "../../../../client/exports/Noop.ts",
|
"@docusaurus/Noop": "../../../../client/exports/Noop.ts",
|
||||||
"@docusaurus/Translate": "../../../../client/exports/Translate.tsx",
|
"@docusaurus/Translate": "../../../../client/exports/Translate.tsx",
|
||||||
|
"@docusaurus/browserContext": "../../../../client/exports/browserContext.tsx",
|
||||||
"@docusaurus/constants": "../../../../client/exports/constants.ts",
|
"@docusaurus/constants": "../../../../client/exports/constants.ts",
|
||||||
"@docusaurus/context": "../../../../client/exports/context.ts",
|
"@docusaurus/docusaurusContext": "../../../../client/exports/docusaurusContext.tsx",
|
||||||
"@docusaurus/isInternalUrl": "../../../../client/exports/isInternalUrl.ts",
|
"@docusaurus/isInternalUrl": "../../../../client/exports/isInternalUrl.ts",
|
||||||
"@docusaurus/renderRoutes": "../../../../client/exports/renderRoutes.ts",
|
"@docusaurus/renderRoutes": "../../../../client/exports/renderRoutes.ts",
|
||||||
"@docusaurus/router": "../../../../client/exports/router.ts",
|
"@docusaurus/router": "../../../../client/exports/router.ts",
|
||||||
"@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts",
|
"@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts",
|
||||||
"@docusaurus/useDocusaurusContext": "../../../../client/exports/useDocusaurusContext.ts",
|
"@docusaurus/useDocusaurusContext": "../../../../client/exports/useDocusaurusContext.ts",
|
||||||
"@docusaurus/useGlobalData": "../../../../client/exports/useGlobalData.ts",
|
"@docusaurus/useGlobalData": "../../../../client/exports/useGlobalData.ts",
|
||||||
|
"@docusaurus/useIsBrowser": "../../../../client/exports/useIsBrowser.ts",
|
||||||
"@generated": "../../../../../../..",
|
"@generated": "../../../../../../..",
|
||||||
"@site": "",
|
"@site": "",
|
||||||
"@theme-init/PluginThemeComponentOverridden": "pluginThemeFolder/PluginThemeComponentOverridden.js",
|
"@theme-init/PluginThemeComponentOverridden": "pluginThemeFolder/PluginThemeComponentOverridden.js",
|
||||||
|
@ -51,13 +53,15 @@ Object {
|
||||||
"@docusaurus/Link": "../../client/exports/Link.tsx",
|
"@docusaurus/Link": "../../client/exports/Link.tsx",
|
||||||
"@docusaurus/Noop": "../../client/exports/Noop.ts",
|
"@docusaurus/Noop": "../../client/exports/Noop.ts",
|
||||||
"@docusaurus/Translate": "../../client/exports/Translate.tsx",
|
"@docusaurus/Translate": "../../client/exports/Translate.tsx",
|
||||||
|
"@docusaurus/browserContext": "../../client/exports/browserContext.tsx",
|
||||||
"@docusaurus/constants": "../../client/exports/constants.ts",
|
"@docusaurus/constants": "../../client/exports/constants.ts",
|
||||||
"@docusaurus/context": "../../client/exports/context.ts",
|
"@docusaurus/docusaurusContext": "../../client/exports/docusaurusContext.tsx",
|
||||||
"@docusaurus/isInternalUrl": "../../client/exports/isInternalUrl.ts",
|
"@docusaurus/isInternalUrl": "../../client/exports/isInternalUrl.ts",
|
||||||
"@docusaurus/renderRoutes": "../../client/exports/renderRoutes.ts",
|
"@docusaurus/renderRoutes": "../../client/exports/renderRoutes.ts",
|
||||||
"@docusaurus/router": "../../client/exports/router.ts",
|
"@docusaurus/router": "../../client/exports/router.ts",
|
||||||
"@docusaurus/useBaseUrl": "../../client/exports/useBaseUrl.ts",
|
"@docusaurus/useBaseUrl": "../../client/exports/useBaseUrl.ts",
|
||||||
"@docusaurus/useDocusaurusContext": "../../client/exports/useDocusaurusContext.ts",
|
"@docusaurus/useDocusaurusContext": "../../client/exports/useDocusaurusContext.ts",
|
||||||
"@docusaurus/useGlobalData": "../../client/exports/useGlobalData.ts",
|
"@docusaurus/useGlobalData": "../../client/exports/useGlobalData.ts",
|
||||||
|
"@docusaurus/useIsBrowser": "../../client/exports/useIsBrowser.ts",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -109,19 +109,56 @@ const Home = () => {
|
||||||
|
|
||||||
### `<BrowserOnly/>` {#browseronly}
|
### `<BrowserOnly/>` {#browseronly}
|
||||||
|
|
||||||
The `<BrowserOnly>` component accepts a `children` prop, a render function which will not be executed during the pre-rendering phase of the build process. This is useful for hiding code that is only meant to run in the browsers (e.g. where the `window`/`document` objects are being accessed). To improve SEO, you can also provide fallback content using the `fallback` prop, which will be prerendered until in the build process and replaced with the client-side only contents when viewed in the browser.
|
The `<BrowserOnly>` component permits to render React components only in the browser, after the React app has hydrated.
|
||||||
|
|
||||||
```jsx {1,5-10}
|
:::tip
|
||||||
|
|
||||||
|
Use it for integrating with code that can't run in Node.js, because `window` or `document` objects are being accessed.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Props {#browseronly-props}
|
||||||
|
|
||||||
|
- `children`: render function prop returning browser-only JSX. Will not be executed in Node.js
|
||||||
|
- `fallback` (optional): JSX to render on the server (Node.js) and until React hydration completes.
|
||||||
|
|
||||||
|
#### Example with code {#browseronly-example-code}
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// highlight-start
|
||||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
|
// highlight-end
|
||||||
|
|
||||||
const MyComponent = () => {
|
const MyComponent = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserOnly
|
// highlight-start
|
||||||
fallback={<div>The fallback content to display on prerendering</div>}>
|
<BrowserOnly>
|
||||||
{() => {
|
{() => {
|
||||||
// Something that should be excluded during build process prerendering.
|
<span>page url = {window.location.href}</span>;
|
||||||
}}
|
}}
|
||||||
</BrowserOnly>
|
</BrowserOnly>
|
||||||
|
// highlight-end
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example with a library {#browseronly-example-library}
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// highlight-start
|
||||||
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
|
// highlight-end
|
||||||
|
|
||||||
|
const MyComponent = (props) => {
|
||||||
|
return (
|
||||||
|
// highlight-start
|
||||||
|
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||||
|
{() => {
|
||||||
|
const LibComponent = require('some-lib').LibComponent;
|
||||||
|
return <LibComponent {...props} />;
|
||||||
|
}}
|
||||||
|
</BrowserOnly>
|
||||||
|
// highlight-end
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -132,7 +169,7 @@ A simple interpolation component for text containing dynamic placeholders.
|
||||||
|
|
||||||
The placeholders will be replaced with the provided dynamic values and JSX elements of your choice (strings, links, styled elements...).
|
The placeholders will be replaced with the provided dynamic values and JSX elements of your choice (strings, links, styled elements...).
|
||||||
|
|
||||||
#### Props {#props}
|
#### Props {#interpolate-props}
|
||||||
|
|
||||||
- `children`: text containing interpolation placeholders like `{placeholderName}`
|
- `children`: text containing interpolation placeholders like `{placeholderName}`
|
||||||
- `values`: object containing interpolation placeholder values
|
- `values`: object containing interpolation placeholder values
|
||||||
|
@ -175,7 +212,7 @@ Apart the `values` prop used for interpolation, it is **not possible to use vari
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### Props {#props-1}
|
#### Props {#translate-props}
|
||||||
|
|
||||||
- `children`: untranslated string in the default site locale (can contain [interpolation placeholders](#interpolate))
|
- `children`: untranslated string in the default site locale (can contain [interpolation placeholders](#interpolate))
|
||||||
- `id`: optional value to use as key in JSON translation files
|
- `id`: optional value to use as key in JSON translation files
|
||||||
|
@ -253,7 +290,6 @@ interface DocusaurusContext {
|
||||||
globalData: Record<string, unknown>;
|
globalData: Record<string, unknown>;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
codeTranslations: Record<string, string>;
|
codeTranslations: Record<string, string>;
|
||||||
isClient: boolean;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -275,6 +311,34 @@ const MyComponent = () => {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `useIsBrowser` {#useIsBrowser}
|
||||||
|
|
||||||
|
Returns `true` when the React app has successfully hydrated in the browser.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic.
|
||||||
|
|
||||||
|
The first client-side render output (in the browser) **must be exactly the same** as the server-side render output (Node.js).
|
||||||
|
|
||||||
|
Not following this rule can lead to unexpected hydration behaviors, as described in [The Perils of Rehydration](https://www.joshwcomeau.com/react/the-perils-of-rehydration/).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React from 'react';
|
||||||
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
// highlight-start
|
||||||
|
const isBrowser = useIsBrowser();
|
||||||
|
// highlight-end
|
||||||
|
return <div>{isBrowser ? 'Client' : 'Server'}</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### `useBaseUrl` {#usebaseurl}
|
### `useBaseUrl` {#usebaseurl}
|
||||||
|
|
||||||
React hook to prepend your site `baseUrl` to a string.
|
React hook to prepend your site `baseUrl` to a string.
|
||||||
|
@ -525,21 +589,27 @@ export default function Home() {
|
||||||
|
|
||||||
### `ExecutionEnvironment` {#executionenvironment}
|
### `ExecutionEnvironment` {#executionenvironment}
|
||||||
|
|
||||||
A module which exposes a few boolean variables to check the current rendering environment. Useful if you want to only run certain code on client/server or need to write server-side rendering compatible code.
|
A module which exposes a few boolean variables to check the current rendering environment.
|
||||||
|
|
||||||
```jsx {2,5}
|
:::caution
|
||||||
import React from 'react';
|
|
||||||
|
For React rendering logic, use [`useIsBrowser()`](#useIsBrowser) or [`<BrowserOnly>`](#browseronly) instead.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
|
||||||
const MyPage = () => {
|
if (ExecutionEnvironment.canUseDOM) {
|
||||||
const location = ExecutionEnvironment.canUseDOM ? window.location.href : null;
|
require('lib-that-only-works-client-side');
|
||||||
return <div>{location}</div>;
|
}
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ExecutionEnvironment.canUseDOM` | `true` if on client, `false` if prerendering. |
|
| `ExecutionEnvironment.canUseDOM` | `true` if on client/browser, `false` on Node.js/prerendering. |
|
||||||
| `ExecutionEnvironment.canUseEventListeners` | `true` if on client and has `window.addEventListener`. |
|
| `ExecutionEnvironment.canUseEventListeners` | `true` if on client and has `window.addEventListener`. |
|
||||||
| `ExecutionEnvironment.canUseIntersectionObserver` | `true` if on client and has `IntersectionObserver`. |
|
| `ExecutionEnvironment.canUseIntersectionObserver` | `true` if on client and has `IntersectionObserver`. |
|
||||||
| `ExecutionEnvironment.canUseViewport` | `true` if on client and has `window.screen`. |
|
| `ExecutionEnvironment.canUseViewport` | `true` if on client and has `window.screen`. |
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue