From 295e77cc098790f89e65d6fcd421dc4e3c6d8eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 12 Aug 2021 19:02:29 +0200 Subject: [PATCH] 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 --- .../src/index.d.ts | 4 + .../src/theme/ThemedImage/index.tsx | 6 +- .../src/theme/DocPage/index.tsx | 3 - .../src/theme/Logo/index.tsx | 2 - .../src/theme/ThemedImage/index.tsx | 6 +- .../src/theme/Toggle/index.tsx | 6 +- .../src/theme/hooks/useWindowSize.ts | 4 - .../src/components/Details/index.tsx | 6 +- .../src/components/Details/styles.module.css | 4 +- .../src/utils/announcementBarUtils.tsx | 6 +- .../src/theme/Playground/index.js | 7 +- packages/docusaurus-types/src/index.d.ts | 5 +- packages/docusaurus/src/client/App.tsx | 42 +++----- .../src/client/exports/BrowserOnly.tsx | 8 +- .../src/client/exports/browserContext.tsx | 32 ++++++ .../src/client/exports/docusaurusContext.tsx | 35 ++++++ .../client/exports/useDocusaurusContext.ts | 9 +- .../exports/{context.ts => useIsBrowser.ts} | 8 +- .../__tests__/__snapshots__/base.test.ts.snap | 8 +- website/docs/docusaurus-core.md | 102 +++++++++++++++--- 20 files changed, 213 insertions(+), 90 deletions(-) create mode 100644 packages/docusaurus/src/client/exports/browserContext.tsx create mode 100644 packages/docusaurus/src/client/exports/docusaurusContext.tsx rename packages/docusaurus/src/client/exports/{context.ts => useIsBrowser.ts} (54%) diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 14b65e17c5..0cdc4a0279 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -179,6 +179,10 @@ declare module '@docusaurus/useDocusaurusContext' { export default function useDocusaurusContext(): DocusaurusContext; } +declare module '@docusaurus/useIsBrowser' { + export default function useIsBrowser(): boolean; +} + declare module '@docusaurus/useBaseUrl' { export type BaseUrlOptions = { forcePrependBaseUrl?: boolean; diff --git a/packages/docusaurus-theme-bootstrap/src/theme/ThemedImage/index.tsx b/packages/docusaurus-theme-bootstrap/src/theme/ThemedImage/index.tsx index cdd8eb52ab..0126d957d0 100644 --- a/packages/docusaurus-theme-bootstrap/src/theme/ThemedImage/index.tsx +++ b/packages/docusaurus-theme-bootstrap/src/theme/ThemedImage/index.tsx @@ -8,14 +8,14 @@ import React from 'react'; import clsx from 'clsx'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import useThemeContext from '@theme/hooks/useThemeContext'; import type {Props} from '@theme/ThemedImage'; import styles from './styles.module.css'; const ThemedImage = (props: Props): JSX.Element => { - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); const {isDarkTheme} = useThemeContext(); const {sources, className, alt = '', ...propsRest} = props; @@ -23,7 +23,7 @@ const ThemedImage = (props: Props): JSX.Element => { const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light']; - const renderedSourceNames: SourceName[] = isClient + const renderedSourceNames: SourceName[] = isBrowser ? clientThemes : // We need to render both images on the server to avoid flash // See https://github.com/facebook/docusaurus/pull/3730 diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index fde80d83a3..77b8f94b28 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -8,7 +8,6 @@ import React, {ReactNode, useState, useCallback} from 'react'; import {MDXProvider} from '@mdx-js/react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import renderRoutes from '@docusaurus/renderRoutes'; import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types'; import Layout from '@theme/Layout'; @@ -37,7 +36,6 @@ function DocPageContent({ versionMetadata, children, }: DocPageContentProps): JSX.Element { - const {isClient} = useDocusaurusContext(); const {pluginId, version} = versionMetadata; const sidebarName = currentDocRoute.sidebar; @@ -57,7 +55,6 @@ function DocPageContent({ return ( { const { siteConfig: {title}, - isClient, } = useDocusaurusContext(); const { navbar: {title: navbarTitle, logo = {src: ''}}, @@ -37,7 +36,6 @@ const Logo = (props: Props): JSX.Element => { {...(logo.target && {target: logo.target})}> {logo.src && ( { - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); const {isDarkTheme} = useThemeContext(); const {sources, className, alt = '', ...propsRest} = props; @@ -23,7 +23,7 @@ const ThemedImage = (props: Props): JSX.Element => { const clientThemes: SourceName[] = isDarkTheme ? ['dark'] : ['light']; - const renderedSourceNames: SourceName[] = isClient + const renderedSourceNames: SourceName[] = isBrowser ? clientThemes : // We need to render both images on the server to avoid flash // See https://github.com/facebook/docusaurus/pull/3730 diff --git a/packages/docusaurus-theme-classic/src/theme/Toggle/index.tsx b/packages/docusaurus-theme-classic/src/theme/Toggle/index.tsx index 251488b17b..8a24f29092 100644 --- a/packages/docusaurus-theme-classic/src/theme/Toggle/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Toggle/index.tsx @@ -8,7 +8,7 @@ import React, {useState, useRef, memo, CSSProperties} from 'react'; import type {Props} from '@theme/Toggle'; import {useThemeConfig} from '@docusaurus/theme-common'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import clsx from 'clsx'; import './styles.css'; @@ -91,11 +91,11 @@ export default function (props: Props): JSX.Element { switchConfig: {darkIcon, darkIconStyle, lightIcon, lightIconStyle}, }, } = useThemeConfig(); - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); return ( , unchecked: , diff --git a/packages/docusaurus-theme-classic/src/theme/hooks/useWindowSize.ts b/packages/docusaurus-theme-classic/src/theme/hooks/useWindowSize.ts index 3447e7a1be..330869965f 100644 --- a/packages/docusaurus-theme-classic/src/theme/hooks/useWindowSize.ts +++ b/packages/docusaurus-theme-classic/src/theme/hooks/useWindowSize.ts @@ -47,10 +47,6 @@ function useWindowSize(): WindowSize { }); useEffect(() => { - if (!ExecutionEnvironment.canUseDOM) { - return undefined; - } - function updateWindowSize() { setWindowSize(getWindowSize()); } diff --git a/packages/docusaurus-theme-common/src/components/Details/index.tsx b/packages/docusaurus-theme-common/src/components/Details/index.tsx index 87f6edec13..6ba2e2012d 100644 --- a/packages/docusaurus-theme-common/src/components/Details/index.tsx +++ b/packages/docusaurus-theme-common/src/components/Details/index.tsx @@ -6,7 +6,7 @@ */ import React, {ComponentProps, ReactElement, useRef, useState} from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import clsx from 'clsx'; import {useCollapsible, Collapsible} from '../Collapsible'; import styles from './styles.module.css'; @@ -30,7 +30,7 @@ export type DetailsProps = { } & ComponentProps<'details'>; const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => { - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); const detailsRef = useRef(null); const {collapsed, setCollapsed} = useCollapsible({ @@ -48,7 +48,7 @@ const Details = ({summary, children, ...props}: DetailsProps): JSX.Element => { data-collapsed={collapsed} className={clsx( styles.details, - {[styles.isClient]: isClient}, + {[styles.isBrowser]: isBrowser}, props.className, )}> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} diff --git a/packages/docusaurus-theme-common/src/components/Details/styles.module.css b/packages/docusaurus-theme-common/src/components/Details/styles.module.css index 4c3eccdfdd..db4eb2ebc1 100644 --- a/packages/docusaurus-theme-common/src/components/Details/styles.module.css +++ b/packages/docusaurus-theme-common/src/components/Details/styles.module.css @@ -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: */ -.details[open]:not(.isClient) > summary:before, +.details[open]:not(.isBrowser) > summary:before, /* 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); } diff --git a/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx b/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx index ae2fde81b8..86eb2ff2b5 100644 --- a/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/announcementBarUtils.tsx @@ -14,7 +14,7 @@ import React, { useContext, createContext, } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import {createStorageSlot} from './storageUtils'; import {useThemeConfig} from './useThemeConfig'; @@ -39,10 +39,10 @@ type AnnouncementBarAPI = { const useAnnouncementBarContextValue = (): AnnouncementBarAPI => { const {announcementBar} = useThemeConfig(); - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); const [isClosed, setClosed] = useState(() => { - return isClient + return isBrowser ? // On client navigation: init with localstorage value isDismissedInStorage() : // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed) diff --git a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.js b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.js index ce2bb86fa9..2b35c04b51 100644 --- a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.js +++ b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.js @@ -10,6 +10,7 @@ import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live'; import clsx from 'clsx'; import Translate from '@docusaurus/Translate'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import usePrismTheme from '@theme/hooks/usePrismTheme'; import styles from './styles.module.css'; @@ -51,8 +52,8 @@ function EditorWithHeader() { } export default function Playground({children, transformCode, ...props}) { + const isBrowser = useIsBrowser(); const { - isClient, siteConfig: { themeConfig: { liveCodeBlock: {playgroundPosition}, @@ -64,8 +65,8 @@ export default function Playground({children, transformCode, ...props}) { return (
`${code};`)} theme={prismTheme} {...props}> diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 81dc328de1..25a1e354bb 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -125,7 +125,10 @@ export interface DocusaurusContext { globalData: Record; i18n: I18n; codeTranslations: Record; - 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 { diff --git a/packages/docusaurus/src/client/App.tsx b/packages/docusaurus/src/client/App.tsx index e14808538b..cd203de487 100644 --- a/packages/docusaurus/src/client/App.tsx +++ b/packages/docusaurus/src/client/App.tsx @@ -5,16 +5,12 @@ * 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 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 DocusaurusContext from './exports/context'; +import {BrowserContextProvider} from './exports/browserContext'; +import {DocusaurusContextProvider} from './exports/docusaurusContext'; import PendingNavigation from './PendingNavigation'; import BaseUrlIssueBanner from './baseUrlIssueBanner/BaseUrlIssueBanner'; import Root from '@theme/Root'; @@ -22,29 +18,17 @@ import Root from '@theme/Root'; import './client-lifecycles-dispatcher'; function App(): JSX.Element { - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - }, []); - return ( - - - - - {renderRoutes(routes)} - - - + + + + + + {renderRoutes(routes)} + + + + ); } diff --git a/packages/docusaurus/src/client/exports/BrowserOnly.tsx b/packages/docusaurus/src/client/exports/BrowserOnly.tsx index 81e56d8b1f..b1166a2957 100644 --- a/packages/docusaurus/src/client/exports/BrowserOnly.tsx +++ b/packages/docusaurus/src/client/exports/BrowserOnly.tsx @@ -6,8 +6,10 @@ */ 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({ children, fallback, @@ -15,9 +17,9 @@ function BrowserOnly({ children?: () => JSX.Element; fallback?: JSX.Element; }): JSX.Element | null { - const {isClient} = useDocusaurusContext(); + const isBrowser = useIsBrowser(); - if (isClient && children != null) { + if (isBrowser && children != null) { return <>{children()}; } diff --git a/packages/docusaurus/src/client/exports/browserContext.tsx b/packages/docusaurus/src/client/exports/browserContext.tsx new file mode 100644 index 0000000000..651db897e1 --- /dev/null +++ b/packages/docusaurus/src/client/exports/browserContext.tsx @@ -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(false); + +export function BrowserContextProvider({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const [isBrowser, setIsBrowser] = useState(false); + + useEffect(() => { + setIsBrowser(true); + }, []); + + return {children}; +} diff --git a/packages/docusaurus/src/client/exports/docusaurusContext.tsx b/packages/docusaurus/src/client/exports/docusaurusContext.tsx new file mode 100644 index 0000000000..a03b6c89ef --- /dev/null +++ b/packages/docusaurus/src/client/exports/docusaurusContext.tsx @@ -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(contextValue); + +export function DocusaurusContextProvider({ + children, +}: { + children: ReactNode; +}): JSX.Element { + return {children}; +} diff --git a/packages/docusaurus/src/client/exports/useDocusaurusContext.ts b/packages/docusaurus/src/client/exports/useDocusaurusContext.ts index 9edc455dac..1b761e38a9 100644 --- a/packages/docusaurus/src/client/exports/useDocusaurusContext.ts +++ b/packages/docusaurus/src/client/exports/useDocusaurusContext.ts @@ -6,16 +6,11 @@ */ import {useContext} from 'react'; -import context from './context'; +import {Context} from './docusaurusContext'; import {DocusaurusContext} from '@docusaurus/types'; function useDocusaurusContext(): DocusaurusContext { - const docusaurusContext = useContext(context); - if (docusaurusContext === null) { - // should not happen normally - throw new Error('Docusaurus context not provided.'); - } - return docusaurusContext; + return useContext(Context); } export default useDocusaurusContext; diff --git a/packages/docusaurus/src/client/exports/context.ts b/packages/docusaurus/src/client/exports/useIsBrowser.ts similarity index 54% rename from packages/docusaurus/src/client/exports/context.ts rename to packages/docusaurus/src/client/exports/useIsBrowser.ts index b770480624..e1e7caff51 100644 --- a/packages/docusaurus/src/client/exports/context.ts +++ b/packages/docusaurus/src/client/exports/useIsBrowser.ts @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import {DocusaurusContext} from '@docusaurus/types'; +import {useContext} from 'react'; +import {Context} from './browserContext'; -export default React.createContext(null); +export default function useIsBrowser(): boolean { + return useContext(Context); +} diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap index ecff7a28d6..1fccb3f231 100644 --- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap +++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap @@ -10,14 +10,16 @@ Object { "@docusaurus/Link": "../../../../client/exports/Link.tsx", "@docusaurus/Noop": "../../../../client/exports/Noop.ts", "@docusaurus/Translate": "../../../../client/exports/Translate.tsx", + "@docusaurus/browserContext": "../../../../client/exports/browserContext.tsx", "@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/renderRoutes": "../../../../client/exports/renderRoutes.ts", "@docusaurus/router": "../../../../client/exports/router.ts", "@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts", "@docusaurus/useDocusaurusContext": "../../../../client/exports/useDocusaurusContext.ts", "@docusaurus/useGlobalData": "../../../../client/exports/useGlobalData.ts", + "@docusaurus/useIsBrowser": "../../../../client/exports/useIsBrowser.ts", "@generated": "../../../../../../..", "@site": "", "@theme-init/PluginThemeComponentOverridden": "pluginThemeFolder/PluginThemeComponentOverridden.js", @@ -51,13 +53,15 @@ Object { "@docusaurus/Link": "../../client/exports/Link.tsx", "@docusaurus/Noop": "../../client/exports/Noop.ts", "@docusaurus/Translate": "../../client/exports/Translate.tsx", + "@docusaurus/browserContext": "../../client/exports/browserContext.tsx", "@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/renderRoutes": "../../client/exports/renderRoutes.ts", "@docusaurus/router": "../../client/exports/router.ts", "@docusaurus/useBaseUrl": "../../client/exports/useBaseUrl.ts", "@docusaurus/useDocusaurusContext": "../../client/exports/useDocusaurusContext.ts", "@docusaurus/useGlobalData": "../../client/exports/useGlobalData.ts", + "@docusaurus/useIsBrowser": "../../client/exports/useIsBrowser.ts", } `; diff --git a/website/docs/docusaurus-core.md b/website/docs/docusaurus-core.md index 841fcee94d..d144820e93 100644 --- a/website/docs/docusaurus-core.md +++ b/website/docs/docusaurus-core.md @@ -109,19 +109,56 @@ const Home = () => { ### `` {#browseronly} -The `` 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 `` 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'; +// highlight-end const MyComponent = () => { return ( - The fallback content to display on prerendering
}> + // highlight-start + {() => { - // Something that should be excluded during build process prerendering. + page url = {window.location.href}; }} + // 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 + Loading...}> + {() => { + const LibComponent = require('some-lib').LibComponent; + return ; + }} + + // 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...). -#### Props {#props} +#### Props {#interpolate-props} - `children`: text containing interpolation placeholders like `{placeholderName}` - `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)) - `id`: optional value to use as key in JSON translation files @@ -253,7 +290,6 @@ interface DocusaurusContext { globalData: Record; i18n: I18n; codeTranslations: Record; - 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
{isBrowser ? 'Client' : 'Server'}
; +}; +``` + ### `useBaseUrl` {#usebaseurl} React hook to prepend your site `baseUrl` to a string. @@ -525,21 +589,27 @@ export default function Home() { ### `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} -import React from 'react'; +:::caution + +For React rendering logic, use [`useIsBrowser()`](#useIsBrowser) or [``](#browseronly) instead. + +::: + +Example: + +```jsx import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; -const MyPage = () => { - const location = ExecutionEnvironment.canUseDOM ? window.location.href : null; - return
{location}
; -}; +if (ExecutionEnvironment.canUseDOM) { + require('lib-that-only-works-client-side'); +} ``` | 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.canUseIntersectionObserver` | `true` if on client and has `IntersectionObserver`. | | `ExecutionEnvironment.canUseViewport` | `true` if on client and has `window.screen`. |