Skip to content

Commit

Permalink
refactor(core): replace useDocusaurusContext().isClient by useIsBrows…
Browse files Browse the repository at this point in the history
…er() (#5349)

* extract separate useIsClient() hook

* for consistency, rename to `useIsBrowser`

* useless return

* improve doc for BrowserOnly

* update snapshot

* polish
  • Loading branch information
slorber authored Aug 12, 2021
1 parent 69b11a8 commit 295e77c
Show file tree
Hide file tree
Showing 20 changed files with 213 additions and 90 deletions.
4 changes: 4 additions & 0 deletions packages/docusaurus-module-type-aliases/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
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;

type SourceName = keyof Props['sources'];

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
Expand Down
3 changes: 0 additions & 3 deletions packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,7 +36,6 @@ function DocPageContent({
versionMetadata,
children,
}: DocPageContentProps): JSX.Element {
const {isClient} = useDocusaurusContext();
const {pluginId, version} = versionMetadata;

const sidebarName = currentDocRoute.sidebar;
Expand All @@ -57,7 +55,6 @@ function DocPageContent({

return (
<Layout
key={`${isClient}`} // TODO seems suspicious
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docPage}
searchMetadatas={{
Expand Down
2 changes: 0 additions & 2 deletions packages/docusaurus-theme-classic/src/theme/Logo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {useThemeConfig} from '@docusaurus/theme-common';
const Logo = (props: Props): JSX.Element => {
const {
siteConfig: {title},
isClient,
} = useDocusaurusContext();
const {
navbar: {title: navbarTitle, logo = {src: ''}},
Expand All @@ -37,7 +36,6 @@ const Logo = (props: Props): JSX.Element => {
{...(logo.target && {target: logo.target})}>
{logo.src && (
<ThemedImage
key={`${isClient}`} // TODO seems suspicious
className={imageClassName}
sources={sources}
alt={logo.alt || navbarTitle || title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
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;

type SourceName = keyof Props['sources'];

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
Expand Down
6 changes: 3 additions & 3 deletions packages/docusaurus-theme-classic/src/theme/Toggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,11 +91,11 @@ export default function (props: Props): JSX.Element {
switchConfig: {darkIcon, darkIconStyle, lightIcon, lightIconStyle},
},
} = useThemeConfig();
const {isClient} = useDocusaurusContext();
const isBrowser = useIsBrowser();

return (
<Toggle
disabled={!isClient}
disabled={!isBrowser}
icons={{
checked: <Dark icon={darkIcon} style={darkIconStyle} />,
unchecked: <Light icon={lightIcon} style={lightIconStyle} />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ function useWindowSize(): WindowSize {
});

useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) {
return undefined;
}

function updateWindowSize() {
setWindowSize(getWindowSize());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<HTMLDetailsElement>(null);

const {collapsed, setCollapsed} = useCollapsible({
Expand All @@ -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 */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -51,8 +52,8 @@ function EditorWithHeader() {
}

export default function Playground({children, transformCode, ...props}) {
const isBrowser = useIsBrowser();
const {
isClient,
siteConfig: {
themeConfig: {
liveCodeBlock: {playgroundPosition},
Expand All @@ -64,8 +65,8 @@ export default function Playground({children, transformCode, ...props}) {
return (
<div className={styles.playgroundContainer}>
<LiveProvider
key={isClient}
code={isClient ? children.replace(/\n$/, '') : ''}
key={isBrowser}
code={isBrowser ? children.replace(/\n$/, '') : ''}
transformCode={transformCode || ((code) => `${code};`)}
theme={prismTheme}
{...props}>
Expand Down
5 changes: 4 additions & 1 deletion packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ export interface DocusaurusContext {
globalData: Record<string, unknown>;
i18n: I18n;
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 {
Expand Down
42 changes: 13 additions & 29 deletions packages/docusaurus/src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,30 @@
* 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';

import './client-lifecycles-dispatcher';

function App(): JSX.Element {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

return (
<DocusaurusContext.Provider
value={{
siteConfig,
siteMetadata,
globalData,
i18n,
codeTranslations,
isClient,
}}>
<Root>
<BaseUrlIssueBanner />
<PendingNavigation routes={routes}>
{renderRoutes(routes)}
</PendingNavigation>
</Root>
</DocusaurusContext.Provider>
<DocusaurusContextProvider>
<BrowserContextProvider>
<Root>
<BaseUrlIssueBanner />
<PendingNavigation routes={routes}>
{renderRoutes(routes)}
</PendingNavigation>
</Root>
</BrowserContextProvider>
</DocusaurusContextProvider>
);
}

Expand Down
8 changes: 5 additions & 3 deletions packages/docusaurus/src/client/exports/BrowserOnly.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
*/

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,
}: {
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()}</>;
}

Expand Down
32 changes: 32 additions & 0 deletions packages/docusaurus/src/client/exports/browserContext.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions packages/docusaurus/src/client/exports/docusaurusContext.tsx
Original file line number Diff line number Diff line change
@@ -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>;
}
Loading

0 comments on commit 295e77c

Please sign in to comment.