Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): replace useDocusaurusContext().isClient by useIsBrowser() #5349

Merged
merged 6 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -85,11 +85,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,
)}
onMouseDown={(e) => {
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