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:
Sébastien Lorber 2021-08-12 19:02:29 +02:00 committed by GitHub
parent 69b11a8546
commit 295e77cc09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 213 additions and 90 deletions

View file

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

View file

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

View file

@ -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={{

View file

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

View file

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

View file

@ -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} />,

View file

@ -47,10 +47,6 @@ function useWindowSize(): WindowSize {
}); });
useEffect(() => { useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) {
return undefined;
}
function updateWindowSize() { function updateWindowSize() {
setWindowSize(getWindowSize()); setWindowSize(getWindowSize());
} }

View file

@ -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 */}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()}</>;
} }

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

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

View file

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

View file

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

View file

@ -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",
} }
`; `;

View file

@ -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`. |