From d71d643aa945d59a289f5b2c152b1d2ba0b41daa Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Sun, 10 Apr 2022 13:41:49 +0800 Subject: [PATCH 01/19] fix: do not fire onRouteUpdate on hash change; new onRouteDidUpdate lifecycle --- packages/docusaurus-types/src/index.d.ts | 4 ++ .../src/client/PendingNavigation.tsx | 37 +++++++++++++------ .../src/client/clientLifecyclesDispatcher.ts | 3 ++ website/_dogfooding/clientModuleExample.ts | 33 ++++++++++++++--- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 348a3ded439e..beb887b885a5 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -599,6 +599,10 @@ export type ClientModule = { previousLocation: Location | null; location: Location; }) => void; + onRouteDidUpdate?: (args: { + previousLocation: Location | null; + location: Location; + }) => void; onRouteUpdateDelayed?: (args: {location: Location}) => void; }; diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 5ebe9217488f..5d3db10f03a4 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -61,24 +61,37 @@ class PendingNavigation extends React.Component { // Load data while the old screen remains. preload(nextLocation.pathname) .then(() => { - clientLifecyclesDispatcher.onRouteUpdate({ - previousLocation: this.previousLocation, - location: nextLocation, - }); - this.setState({nextRouteHasLoaded: true}, this.stopProgressBar); - const {hash} = nextLocation; - if (!hash) { - window.scrollTo(0, 0); - } else { - const id = decodeURIComponent(hash.substring(1)); - const element = document.getElementById(id); - element?.scrollIntoView(); + if (this.previousLocation?.pathname !== nextLocation.pathname) { + clientLifecyclesDispatcher.onRouteUpdate({ + previousLocation: this.previousLocation, + location: nextLocation, + }); } + this.setState({nextRouteHasLoaded: true}, this.stopProgressBar); }) .catch((e) => console.warn(e)); return false; } + override componentDidUpdate(): void { + if (this.previousLocation !== this.props.location) { + const {hash} = this.props.location; + if (!hash) { + window.scrollTo(0, 0); + } else { + const id = decodeURIComponent(hash.substring(1)); + const element = document.getElementById(id); + element?.scrollIntoView(); + } + } + if (this.previousLocation?.pathname !== this.props.location.pathname) { + clientLifecyclesDispatcher.onRouteDidUpdate({ + previousLocation: this.previousLocation, + location: this.props.location, + }); + } + } + private clearProgressBarTimeout() { if (this.progressBarTimeout) { window.clearTimeout(this.progressBarTimeout); diff --git a/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts b/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts index 57982c394313..a90373cd5c39 100644 --- a/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts +++ b/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts @@ -26,6 +26,9 @@ const clientLifecyclesDispatchers: Required = { onRouteUpdate(...args) { dispatchLifecycleAction('onRouteUpdate', args); }, + onRouteDidUpdate(...args) { + dispatchLifecycleAction('onRouteDidUpdate', args); + }, onRouteUpdateDelayed(...args) { dispatchLifecycleAction('onRouteUpdateDelayed', args); }, diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index e730f3f3a74e..94981e6581d0 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -6,12 +6,33 @@ */ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import type {Location} from 'history'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function onRouteUpdate({location}: {location: Location}): void { - // console.log('onRouteUpdate', {location}); +export function onRouteUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location; +}): void { + if (ExecutionEnvironment.canUseDOM) { + console.log(`onRouteUpdate (Fired before DOM repaints) +Previous location: ${previousLocation.pathname} +Current location: ${location.pathname} +Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); + } } - -if (ExecutionEnvironment.canUseDOM) { - // console.log('client module example log'); +export function onRouteDidUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location; +}): void { + if (ExecutionEnvironment.canUseDOM) { + console.log(`onRouteDidUpdate (Fired after DOM repaints) +Previous location: ${previousLocation.pathname} +Current location: ${location.pathname} +Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); + } } From a4e16b9307345f9b6fb556fc13e33eca6e54cd80 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Sun, 10 Apr 2022 14:24:34 +0800 Subject: [PATCH 02/19] use a separate dispatcher layer --- packages/docusaurus-types/src/index.d.ts | 4 -- packages/docusaurus/src/client/App.tsx | 2 +- .../src/client/ClientLifecyclesDispatcher.tsx | 66 +++++++++++++++++++ .../src/client/PendingNavigation.tsx | 47 +++++-------- .../src/client/clientLifecyclesDispatcher.ts | 37 ----------- website/_dogfooding/clientModuleExample.ts | 18 ++--- 6 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx delete mode 100644 packages/docusaurus/src/client/clientLifecyclesDispatcher.ts diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index beb887b885a5..348a3ded439e 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -599,10 +599,6 @@ export type ClientModule = { previousLocation: Location | null; location: Location; }) => void; - onRouteDidUpdate?: (args: { - previousLocation: Location | null; - location: Location; - }) => void; onRouteUpdateDelayed?: (args: {location: Location}) => void; }; diff --git a/packages/docusaurus/src/client/App.tsx b/packages/docusaurus/src/client/App.tsx index ca367d6bce3f..a50b716f9678 100644 --- a/packages/docusaurus/src/client/App.tsx +++ b/packages/docusaurus/src/client/App.tsx @@ -19,7 +19,7 @@ import SiteMetadataDefaults from './SiteMetadataDefaults'; import Root from '@theme/Root'; import SiteMetadata from '@theme/SiteMetadata'; -import './clientLifecyclesDispatcher'; +import '@generated/client-modules'; // TODO, quick fix for CSS insertion order import ErrorBoundary from '@docusaurus/ErrorBoundary'; diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx new file mode 100644 index 000000000000..e426038c5483 --- /dev/null +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -0,0 +1,66 @@ +/** + * 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, { + useEffect, + useImperativeHandle, + useLayoutEffect, + type ReactNode, +} from 'react'; +import clientModules from '@generated/client-modules'; +import type {ClientModule} from '@docusaurus/types'; +import type {Location} from 'history'; + +function dispatchLifecycleAction( + lifecycleAction: K, + ...args: Parameters> +) { + clientModules.forEach((clientModule) => { + const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ?? + clientModule[lifecycleAction]) as + | ((...a: Parameters>) => void) + | undefined; + + lifecycleFunction?.(...args); + }); +} + +function ClientLifecyclesDispatcher( + { + children, + location, + previousLocation, + }: { + children: ReactNode; + location: Location; + previousLocation: Location | null; + }, + ref: React.ForwardedRef, +): JSX.Element { + useEffect(() => { + if (previousLocation !== location) { + const {hash} = location; + if (!hash) { + window.scrollTo(0, 0); + } else { + const id = decodeURIComponent(hash.substring(1)); + const element = document.getElementById(id); + element?.scrollIntoView(); + } + } + }); + useLayoutEffect(() => { + dispatchLifecycleAction('onRouteUpdate', {previousLocation, location}); + }); + useImperativeHandle(ref, () => ({ + onRouteUpdateDelayed: () => + dispatchLifecycleAction('onRouteUpdateDelayed', {location}), + })); + return <>{children}; +} + +export default React.forwardRef(ClientLifecyclesDispatcher); diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 5d3db10f03a4..bb89c2697cef 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -9,9 +9,10 @@ import React from 'react'; import {Route} from 'react-router-dom'; import nprogress from 'nprogress'; -import clientLifecyclesDispatcher from './clientLifecyclesDispatcher'; +import ClientLifecyclesDispatcher from './ClientLifecyclesDispatcher'; import preload from './preload'; import type {Location} from 'history'; +import type {ClientModule} from '@docusaurus/types'; import './nprogress.css'; @@ -29,6 +30,7 @@ type State = { class PendingNavigation extends React.Component { private previousLocation: Location | null; private progressBarTimeout: number | null; + private clientLifecyclesDispatcher: React.RefObject>; constructor(props: Props) { super(props); @@ -36,6 +38,7 @@ class PendingNavigation extends React.Component { // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; this.progressBarTimeout = null; + this.clientLifecyclesDispatcher = React.createRef(); this.state = { nextRouteHasLoaded: true, }; @@ -60,38 +63,13 @@ class PendingNavigation extends React.Component { // Load data while the old screen remains. preload(nextLocation.pathname) - .then(() => { - if (this.previousLocation?.pathname !== nextLocation.pathname) { - clientLifecyclesDispatcher.onRouteUpdate({ - previousLocation: this.previousLocation, - location: nextLocation, - }); - } - this.setState({nextRouteHasLoaded: true}, this.stopProgressBar); - }) + .then(() => + this.setState({nextRouteHasLoaded: true}, this.stopProgressBar), + ) .catch((e) => console.warn(e)); return false; } - override componentDidUpdate(): void { - if (this.previousLocation !== this.props.location) { - const {hash} = this.props.location; - if (!hash) { - window.scrollTo(0, 0); - } else { - const id = decodeURIComponent(hash.substring(1)); - const element = document.getElementById(id); - element?.scrollIntoView(); - } - } - if (this.previousLocation?.pathname !== this.props.location.pathname) { - clientLifecyclesDispatcher.onRouteDidUpdate({ - previousLocation: this.previousLocation, - location: this.props.location, - }); - } - } - private clearProgressBarTimeout() { if (this.progressBarTimeout) { window.clearTimeout(this.progressBarTimeout); @@ -102,7 +80,7 @@ class PendingNavigation extends React.Component { private startProgressBar() { this.clearProgressBarTimeout(); this.progressBarTimeout = window.setTimeout(() => { - clientLifecyclesDispatcher.onRouteUpdateDelayed({ + this.clientLifecyclesDispatcher.current!.onRouteUpdateDelayed({ location: this.props.location, }); nprogress.start(); @@ -118,7 +96,14 @@ class PendingNavigation extends React.Component { const {children, location} = this.props; // Use a controlled to trick all descendants into rendering the old // location. - return children} />; + return ( + + children} /> + + ); } } diff --git a/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts b/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts deleted file mode 100644 index a90373cd5c39..000000000000 --- a/packages/docusaurus/src/client/clientLifecyclesDispatcher.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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 clientModules from '@generated/client-modules'; -import type {ClientModule} from '@docusaurus/types'; - -function dispatchLifecycleAction( - lifecycleAction: K, - args: Parameters>, -) { - clientModules.forEach((clientModule) => { - const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ?? - clientModule[lifecycleAction]) as - | ((...a: Parameters>) => void) - | undefined; - - lifecycleFunction?.(...args); - }); -} - -const clientLifecyclesDispatchers: Required = { - onRouteUpdate(...args) { - dispatchLifecycleAction('onRouteUpdate', args); - }, - onRouteDidUpdate(...args) { - dispatchLifecycleAction('onRouteDidUpdate', args); - }, - onRouteUpdateDelayed(...args) { - dispatchLifecycleAction('onRouteUpdateDelayed', args); - }, -}; - -export default clientLifecyclesDispatchers; diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index 94981e6581d0..81634b53c618 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -13,25 +13,19 @@ export function onRouteUpdate({ previousLocation, }: { location: Location; - previousLocation: Location; + previousLocation: Location | null; }): void { if (ExecutionEnvironment.canUseDOM) { - console.log(`onRouteUpdate (Fired before DOM repaints) -Previous location: ${previousLocation.pathname} + console.log(`onRouteUpdate +Previous location: ${previousLocation?.pathname} Current location: ${location.pathname} Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); } } -export function onRouteDidUpdate({ - location, - previousLocation, -}: { - location: Location; - previousLocation: Location; -}): void { + +export function onRouteUpdateDelayed({location}: {location: Location}): void { if (ExecutionEnvironment.canUseDOM) { - console.log(`onRouteDidUpdate (Fired after DOM repaints) -Previous location: ${previousLocation.pathname} + console.log(`onRouteUpdateDelayed Current location: ${location.pathname} Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); } From f33fb16bbed2a88b0cde322cdeb4fa85a039e7f1 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 08:48:24 +0800 Subject: [PATCH 03/19] address reviews --- .../src/client/ClientLifecyclesDispatcher.tsx | 9 +++------ website/_dogfooding/clientModuleExample.ts | 11 +++++++++-- website/docusaurus.config.js | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx index e426038c5483..8ddf486356fb 100644 --- a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -6,7 +6,6 @@ */ import React, { - useEffect, useImperativeHandle, useLayoutEffect, type ReactNode, @@ -41,7 +40,7 @@ function ClientLifecyclesDispatcher( }, ref: React.ForwardedRef, ): JSX.Element { - useEffect(() => { + useLayoutEffect(() => { if (previousLocation !== location) { const {hash} = location; if (!hash) { @@ -51,11 +50,9 @@ function ClientLifecyclesDispatcher( const element = document.getElementById(id); element?.scrollIntoView(); } + dispatchLifecycleAction('onRouteUpdate', {previousLocation, location}); } - }); - useLayoutEffect(() => { - dispatchLifecycleAction('onRouteUpdate', {previousLocation, location}); - }); + }, [previousLocation, location]); useImperativeHandle(ref, () => ({ onRouteUpdateDelayed: () => dispatchLifecycleAction('onRouteUpdateDelayed', {location}), diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index 81634b53c618..9a00688a70aa 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -6,6 +6,7 @@ */ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import siteConfig from '@generated/docusaurus.config'; import type {Location} from 'history'; export function onRouteUpdate({ @@ -15,7 +16,10 @@ export function onRouteUpdate({ location: Location; previousLocation: Location | null; }): void { - if (ExecutionEnvironment.canUseDOM) { + if ( + siteConfig.customFields!.isDeployPreview && + ExecutionEnvironment.canUseDOM + ) { console.log(`onRouteUpdate Previous location: ${previousLocation?.pathname} Current location: ${location.pathname} @@ -24,7 +28,10 @@ Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); } export function onRouteUpdateDelayed({location}: {location: Location}): void { - if (ExecutionEnvironment.canUseDOM) { + if ( + siteConfig.customFields!.isDeployPreview && + ExecutionEnvironment.canUseDOM + ) { console.log(`onRouteUpdateDelayed Current location: ${location.pathname} Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ff88fba0232e..4fa2e568d011 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -113,6 +113,7 @@ const config = { onBrokenMarkdownLinks: 'warn', favicon: 'img/docusaurus.ico', customFields: { + isDeployPreview, description: 'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.', }, From d725324ec4e4061ec05f6865d11dc897e5c51bf5 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 08:56:41 +0800 Subject: [PATCH 04/19] move this up --- packages/docusaurus/src/client/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/docusaurus/src/client/App.tsx b/packages/docusaurus/src/client/App.tsx index a50b716f9678..f32788f53bab 100644 --- a/packages/docusaurus/src/client/App.tsx +++ b/packages/docusaurus/src/client/App.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import '@generated/client-modules'; import routes from '@generated/routes'; import {useLocation} from '@docusaurus/router'; @@ -19,8 +20,6 @@ import SiteMetadataDefaults from './SiteMetadataDefaults'; import Root from '@theme/Root'; import SiteMetadata from '@theme/SiteMetadata'; -import '@generated/client-modules'; - // TODO, quick fix for CSS insertion order import ErrorBoundary from '@docusaurus/ErrorBoundary'; import Error from '@theme/Error'; From b581e6d413df55b4efaadc8f3b108ac52b8a634f Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 12:06:53 +0800 Subject: [PATCH 05/19] minor refactor --- .../docusaurus/src/client/ClientLifecyclesDispatcher.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx index 8ddf486356fb..851b9d7903da 100644 --- a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -8,7 +8,7 @@ import React, { useImperativeHandle, useLayoutEffect, - type ReactNode, + type ReactElement, } from 'react'; import clientModules from '@generated/client-modules'; import type {ClientModule} from '@docusaurus/types'; @@ -34,7 +34,7 @@ function ClientLifecyclesDispatcher( location, previousLocation, }: { - children: ReactNode; + children: ReactElement; location: Location; previousLocation: Location | null; }, @@ -57,7 +57,7 @@ function ClientLifecyclesDispatcher( onRouteUpdateDelayed: () => dispatchLifecycleAction('onRouteUpdateDelayed', {location}), })); - return <>{children}; + return children; } export default React.forwardRef(ClientLifecyclesDispatcher); From 403d5f002e82196293069f62851e2fe252b222e7 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 16:43:50 +0800 Subject: [PATCH 06/19] add docs --- website/docs/advanced/client.md | 34 ++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/website/docs/advanced/client.md b/website/docs/advanced/client.md index ef4c4d220c60..2673f845a859 100644 --- a/website/docs/advanced/client.md +++ b/website/docs/advanced/client.md @@ -86,7 +86,7 @@ Client modules are part of your site's bundle, just like theme components. Howev These modules are imported globally before React even renders the initial UI. -```js title="App.tsx" +```js title="@docusaurus/core/App.tsx" // How it works under the hood import '@generated/client-modules'; ``` @@ -117,5 +117,33 @@ CSS stylesheets imported as client modules are [global](../styling-layout.md#glo } ``` - - +### Client module lifecycles {#client-module-lifecycles} + +Besides introducing side-effects, client modules can optionally export one function: `onRouteUpdate`. + +Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not be re-executed on page transitions. `onRouteUpdate` is useful if you have some imperative JS logic that should be immediately applied once a new page has loaded, for example, to manipulate certain DOM elements. + +```ts title="myClientModule.ts" +import type {Location} from 'history'; + +export function onRouteUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location | null; +}): void { + const title = document.getElementsByTagName('h1')[0]; + if (title) { + title.innerText += '❤️'; + } +} +``` + +The lifecycle will be fired as soon as the DOM on the new page has mounted. + +:::tip Prefer using React + +Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.md) instead. + +::: From 297e149dc5c2c527f8a6fa94d1a103e427e1be01 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 18:25:12 +0800 Subject: [PATCH 07/19] move nprogress to theme-classic --- .../docusaurus-theme-classic/package.json | 2 + .../docusaurus-theme-classic/src/index.ts | 1 + .../src}/nprogress.css | 5 ++- .../docusaurus-theme-classic/src/nprogress.ts | 34 ++++++++++++++ .../docusaurus-theme-classic/tsconfig.json | 3 +- packages/docusaurus-types/src/index.d.ts | 7 ++- packages/docusaurus/package.json | 2 - packages/docusaurus/src/client/App.tsx | 4 +- .../src/client/ClientLifecyclesDispatcher.tsx | 16 ++++--- .../src/client/PendingNavigation.tsx | 44 ++++-------------- project-words.txt | 1 + website/_dogfooding/clientModuleExample.ts | 41 +++++++++++------ website/docs/advanced/client.md | 45 ++++++++++++++++--- 13 files changed, 131 insertions(+), 74 deletions(-) rename packages/{docusaurus/src/client => docusaurus-theme-classic/src}/nprogress.css (83%) create mode 100644 packages/docusaurus-theme-classic/src/nprogress.ts diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index a41328ecb127..bb63b12fd689 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -35,6 +35,7 @@ "copy-text-to-clipboard": "^3.0.1", "infima": "0.2.0-alpha.38", "lodash": "^4.17.21", + "nprogress": "^0.2.0", "postcss": "^8.4.12", "prism-react-renderer": "^1.3.1", "prismjs": "^1.28.0", @@ -48,6 +49,7 @@ "@docusaurus/module-type-aliases": "2.0.0-beta.18", "@docusaurus/types": "2.0.0-beta.18", "@types/mdx-js__react": "^1.5.5", + "@types/nprogress": "^0.2.0", "@types/prismjs": "^1.26.0", "@types/rtlcss": "^3.1.4", "cross-env": "^7.0.3", diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index 8aa1c7b805ad..d24c0b3f87e3 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -138,6 +138,7 @@ export default function docusaurusThemeClassic( require.resolve(getInfimaCSSFile(direction)), './prism-include-languages', './admonitions.css', + './nprogress', ]; if (customCss) { diff --git a/packages/docusaurus/src/client/nprogress.css b/packages/docusaurus-theme-classic/src/nprogress.css similarity index 83% rename from packages/docusaurus/src/client/nprogress.css rename to packages/docusaurus-theme-classic/src/nprogress.css index a6a0ffbf1cd6..c354979d2044 100644 --- a/packages/docusaurus/src/client/nprogress.css +++ b/packages/docusaurus-theme-classic/src/nprogress.css @@ -16,7 +16,7 @@ } #nprogress .bar { - background: #29d; + background: var(--ifm-color-primary); position: fixed; z-index: 1031; top: 0; @@ -30,7 +30,8 @@ right: 0; width: 100px; height: 100%; - box-shadow: 0 0 10px #29d, 0 0 5px #29d; + box-shadow: 0 0 10px var(--ifm-color-primary), + 0 0 5px var(--ifm-color-primary); opacity: 1; transform: rotate(3deg) translate(0, -4px); } diff --git a/packages/docusaurus-theme-classic/src/nprogress.ts b/packages/docusaurus-theme-classic/src/nprogress.ts new file mode 100644 index 000000000000..4202032b33a5 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/nprogress.ts @@ -0,0 +1,34 @@ +/** + * 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 nprogress from 'nprogress'; +import './nprogress.css'; +import type {Location} from 'history'; + +nprogress.configure({showSpinner: false}); + +const delay = 200; + +export function onRouteUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location | null; +}): (() => void) | undefined { + if (location.pathname !== previousLocation?.pathname) { + const progressBarTimeout = window.setTimeout(() => { + nprogress.start(); + }, delay); + return () => window.clearTimeout(progressBarTimeout); + } + return undefined; +} + +export function onRouteDidUpdate(): void { + nprogress.done(); +} diff --git a/packages/docusaurus-theme-classic/tsconfig.json b/packages/docusaurus-theme-classic/tsconfig.json index ab5531d36d62..145e89f84a44 100644 --- a/packages/docusaurus-theme-classic/tsconfig.json +++ b/packages/docusaurus-theme-classic/tsconfig.json @@ -4,8 +4,7 @@ "lib": ["DOM", "ES2019"], "module": "esnext", "noEmit": true, - "jsx": "react-native", - "baseUrl": "src" + "jsx": "react-native" }, "include": ["src/"] } diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index a3663572b9e0..a4c187396207 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -601,11 +601,14 @@ export type TOCItem = { }; export type ClientModule = { + onRouteDidUpdate?: (args: { + previousLocation: Location | null; + location: Location; + }) => (() => void) | void; onRouteUpdate?: (args: { previousLocation: Location | null; location: Location; - }) => void; - onRouteUpdateDelayed?: (args: {location: Location}) => void; + }) => (() => void) | void; }; /** What the user configures. */ diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 7102beef0b66..eba4103a4950 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -77,7 +77,6 @@ "leven": "^3.1.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.6.0", - "nprogress": "^0.2.0", "postcss": "^8.4.12", "postcss-loader": "^6.2.1", "prompts": "^2.4.2", @@ -108,7 +107,6 @@ "@docusaurus/module-type-aliases": "2.0.0-beta.18", "@docusaurus/types": "2.0.0-beta.18", "@types/detect-port": "^1.3.2", - "@types/nprogress": "^0.2.0", "@types/react-dom": "^18.0.2", "@types/react-router-config": "^5.0.6", "@types/rtl-detect": "^1.0.0", diff --git a/packages/docusaurus/src/client/App.tsx b/packages/docusaurus/src/client/App.tsx index f32788f53bab..c709c2e8515f 100644 --- a/packages/docusaurus/src/client/App.tsx +++ b/packages/docusaurus/src/client/App.tsx @@ -35,9 +35,7 @@ export default function App(): JSX.Element { - + {routeElement} diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx index 851b9d7903da..5bb771ce188e 100644 --- a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -17,15 +17,18 @@ import type {Location} from 'history'; function dispatchLifecycleAction( lifecycleAction: K, ...args: Parameters> -) { - clientModules.forEach((clientModule) => { +): () => void { + const callbacks = clientModules.map((clientModule) => { const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ?? clientModule[lifecycleAction]) as - | ((...a: Parameters>) => void) + | (( + ...a: Parameters> + ) => (() => void) | void) | undefined; - lifecycleFunction?.(...args); + return lifecycleFunction?.(...args); }); + return () => callbacks.forEach((cb) => cb?.()); } function ClientLifecyclesDispatcher( @@ -50,12 +53,11 @@ function ClientLifecyclesDispatcher( const element = document.getElementById(id); element?.scrollIntoView(); } - dispatchLifecycleAction('onRouteUpdate', {previousLocation, location}); + dispatchLifecycleAction('onRouteDidUpdate', {previousLocation, location}); } }, [previousLocation, location]); useImperativeHandle(ref, () => ({ - onRouteUpdateDelayed: () => - dispatchLifecycleAction('onRouteUpdateDelayed', {location}), + onRouteUpdate: (args) => dispatchLifecycleAction('onRouteUpdate', args), })); return children; } diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index bb89c2697cef..df1b0df282bb 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -7,19 +7,12 @@ import React from 'react'; import {Route} from 'react-router-dom'; -import nprogress from 'nprogress'; - import ClientLifecyclesDispatcher from './ClientLifecyclesDispatcher'; import preload from './preload'; import type {Location} from 'history'; import type {ClientModule} from '@docusaurus/types'; -import './nprogress.css'; - -nprogress.configure({showSpinner: false}); - type Props = { - readonly delay: number; readonly location: Location; readonly children: JSX.Element; }; @@ -29,7 +22,7 @@ type State = { class PendingNavigation extends React.Component { private previousLocation: Location | null; - private progressBarTimeout: number | null; + private previousRouteUpdateCallback: (() => void) | null; private clientLifecyclesDispatcher: React.RefObject>; constructor(props: Props) { @@ -37,7 +30,7 @@ class PendingNavigation extends React.Component { // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; - this.progressBarTimeout = null; + this.previousRouteUpdateCallback = null; this.clientLifecyclesDispatcher = React.createRef(); this.state = { nextRouteHasLoaded: true, @@ -59,41 +52,22 @@ class PendingNavigation extends React.Component { // Save the location first. this.previousLocation = this.props.location; this.setState({nextRouteHasLoaded: false}); - this.startProgressBar(); + this.previousRouteUpdateCallback = + this.clientLifecyclesDispatcher.current?.onRouteUpdate({ + previousLocation: this.previousLocation, + location: nextLocation, + }) || null; // Load data while the old screen remains. preload(nextLocation.pathname) - .then(() => - this.setState({nextRouteHasLoaded: true}, this.stopProgressBar), - ) + .then(() => this.setState({nextRouteHasLoaded: true})) .catch((e) => console.warn(e)); return false; } - private clearProgressBarTimeout() { - if (this.progressBarTimeout) { - window.clearTimeout(this.progressBarTimeout); - this.progressBarTimeout = null; - } - } - - private startProgressBar() { - this.clearProgressBarTimeout(); - this.progressBarTimeout = window.setTimeout(() => { - this.clientLifecyclesDispatcher.current!.onRouteUpdateDelayed({ - location: this.props.location, - }); - nprogress.start(); - }, this.props.delay); - } - - private stopProgressBar() { - this.clearProgressBarTimeout(); - nprogress.done(); - } - override render(): JSX.Element { const {children, location} = this.props; + this.previousRouteUpdateCallback?.(); // Use a controlled to trick all descendants into rendering the old // location. return ( diff --git a/project-words.txt b/project-words.txt index 5bd78cccc06e..9608214fdde2 100644 --- a/project-words.txt +++ b/project-words.txt @@ -215,6 +215,7 @@ preconfigured preconnect prefetch prefetching +preloads prepended preprocessors prerendered diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index 9a00688a70aa..01b2d8250631 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -5,35 +5,48 @@ * LICENSE file in the root directory of this source tree. */ -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import siteConfig from '@generated/docusaurus.config'; import type {Location} from 'history'; +function logPage( + event: string, + location: Location, + previousLocation: Location | null, +): void { + console.log(`${event} +Previous location: ${previousLocation?.pathname} +Current location: ${location.pathname} +Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); +} + export function onRouteUpdate({ location, previousLocation, }: { location: Location; previousLocation: Location | null; -}): void { +}): (() => void) | void { if ( - siteConfig.customFields!.isDeployPreview && - ExecutionEnvironment.canUseDOM + process.env.NODE_ENV === 'development' || + siteConfig.customFields!.isDeployPreview ) { - console.log(`onRouteUpdate -Previous location: ${previousLocation?.pathname} -Current location: ${location.pathname} -Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); + logPage('onRouteUpdate', location, previousLocation); + return () => logPage('onRouteUpdate cleanup', location, previousLocation); } + return undefined; } -export function onRouteUpdateDelayed({location}: {location: Location}): void { +export function onRouteDidUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location | null; +}): void { if ( - siteConfig.customFields!.isDeployPreview && - ExecutionEnvironment.canUseDOM + process.env.NODE_ENV === 'development' || + siteConfig.customFields!.isDeployPreview ) { - console.log(`onRouteUpdateDelayed -Current location: ${location.pathname} -Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`); + logPage('onRouteDidUpdate', location, previousLocation); } } diff --git a/website/docs/advanced/client.md b/website/docs/advanced/client.md index 2673f845a859..4b5c644d2062 100644 --- a/website/docs/advanced/client.md +++ b/website/docs/advanced/client.md @@ -119,13 +119,43 @@ CSS stylesheets imported as client modules are [global](../styling-layout.md#glo ### Client module lifecycles {#client-module-lifecycles} -Besides introducing side-effects, client modules can optionally export one function: `onRouteUpdate`. +Besides introducing side-effects, client modules can optionally export two functions: `onRouteUpdate` and `onRouteDidUpdate`. -Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not be re-executed on page transitions. `onRouteUpdate` is useful if you have some imperative JS logic that should be immediately applied once a new page has loaded, for example, to manipulate certain DOM elements. +Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not be re-executed on page transitions. These lifecycles are useful if you have some imperative JS logic that should be executed every time a new page has loaded, for example, to manipulate DOM elements, to send analytics data, etc. + +For every route transition, there will be several important events: + +1. The user clicks a link, which causes the router to change its current location. +2. Docusaurus preloads the next route's assets, while keeping displaying the current page's content. +3. The next route's assets have loaded. +4. The new location's route component gets rendered to DOM. + +`onRouteUpdate` will be called at event (2), and `onRouteDidUpdate` will be called at (4). They both receive the previous location (which can be `null`, if this is the first screen). + +`onRouteUpdate` can optionally return a "cleanup" callback, which will be called at (3). For example, if you want to display a progress bar, you can start a timeout in `onRouteUpdate`, and clear the timeout in the callback. (The classic theme already provides an `nprogress` integration this way.) + +Note that the new page's DOM is only available during event (4). If you need to manipulate the new page's DOM, you'll likely want to use `onRouteDidUpdate`, which will be fired as soon as the DOM on the new page has mounted. ```ts title="myClientModule.ts" import type {Location} from 'history'; +export function onRouteDidUpdate({ + location, + previousLocation, +}: { + location: Location; + previousLocation: Location | null; +}) { + // Don't execute if we are still on the same page; the lifecycle may be fired + // because the hash changes (e.g. when navigating between headings) + if (location.pathname !== previousLocation?.pathname) { + const title = document.getElementsByTagName('h1')[0]; + if (title) { + title.innerText += '❤️'; + } + } +} + export function onRouteUpdate({ location, previousLocation, @@ -133,15 +163,16 @@ export function onRouteUpdate({ location: Location; previousLocation: Location | null; }): void { - const title = document.getElementsByTagName('h1')[0]; - if (title) { - title.innerText += '❤️'; + if (location.pathname !== previousLocation?.pathname) { + const progressBarTimeout = window.setTimeout(() => { + nprogress.start(); + }, delay); + return () => window.clearTimeout(progressBarTimeout); } + return undefined; } ``` -The lifecycle will be fired as soon as the DOM on the new page has mounted. - :::tip Prefer using React Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.md) instead. From c2410eac99b41158b73e4d05b09ff01da4c39c11 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 18:40:16 +0800 Subject: [PATCH 08/19] move cb --- packages/docusaurus/src/client/PendingNavigation.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index df1b0df282bb..976eb0e3d1bf 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -22,7 +22,7 @@ type State = { class PendingNavigation extends React.Component { private previousLocation: Location | null; - private previousRouteUpdateCallback: (() => void) | null; + private routeUpdateCb: (() => void) | undefined; private clientLifecyclesDispatcher: React.RefObject>; constructor(props: Props) { @@ -30,7 +30,7 @@ class PendingNavigation extends React.Component { // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; - this.previousRouteUpdateCallback = null; + this.routeUpdateCb = undefined; this.clientLifecyclesDispatcher = React.createRef(); this.state = { nextRouteHasLoaded: true, @@ -52,22 +52,21 @@ class PendingNavigation extends React.Component { // Save the location first. this.previousLocation = this.props.location; this.setState({nextRouteHasLoaded: false}); - this.previousRouteUpdateCallback = + this.routeUpdateCb = this.clientLifecyclesDispatcher.current?.onRouteUpdate({ previousLocation: this.previousLocation, location: nextLocation, - }) || null; + }) || undefined; // Load data while the old screen remains. preload(nextLocation.pathname) - .then(() => this.setState({nextRouteHasLoaded: true})) + .then(() => this.setState({nextRouteHasLoaded: true}, this.routeUpdateCb)) .catch((e) => console.warn(e)); return false; } override render(): JSX.Element { const {children, location} = this.props; - this.previousRouteUpdateCallback?.(); // Use a controlled to trick all descendants into rendering the old // location. return ( From 004d761c3257716b0e04ae592cb5c04340a06623 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 19:04:43 +0800 Subject: [PATCH 09/19] move it back... --- packages/docusaurus/src/client/PendingNavigation.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 976eb0e3d1bf..eaff76d68fe4 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -60,13 +60,14 @@ class PendingNavigation extends React.Component { // Load data while the old screen remains. preload(nextLocation.pathname) - .then(() => this.setState({nextRouteHasLoaded: true}, this.routeUpdateCb)) + .then(() => this.setState({nextRouteHasLoaded: true})) .catch((e) => console.warn(e)); return false; } override render(): JSX.Element { const {children, location} = this.props; + this.routeUpdateCb?.(); // Use a controlled to trick all descendants into rendering the old // location. return ( From 773e580a84832c0de25b376395026f5801e2f856 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 20:47:54 +0800 Subject: [PATCH 10/19] minor doc change --- website/docs/advanced/client.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/advanced/client.md b/website/docs/advanced/client.md index 4b5c644d2062..ac87fcd8371e 100644 --- a/website/docs/advanced/client.md +++ b/website/docs/advanced/client.md @@ -119,18 +119,18 @@ CSS stylesheets imported as client modules are [global](../styling-layout.md#glo ### Client module lifecycles {#client-module-lifecycles} -Besides introducing side-effects, client modules can optionally export two functions: `onRouteUpdate` and `onRouteDidUpdate`. +Besides introducing side-effects, client modules can optionally export two lifecycle functions: `onRouteUpdate` and `onRouteDidUpdate`. -Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not be re-executed on page transitions. These lifecycles are useful if you have some imperative JS logic that should be executed every time a new page has loaded, for example, to manipulate DOM elements, to send analytics data, etc. +Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not re-execute on page transitions. These lifecycles are useful if you have some imperative JS logic that should execute every time a new page has loaded, e.g., to manipulate DOM elements, to send analytics data, etc. -For every route transition, there will be several important events: +For every route transition, there will be several important timings: 1. The user clicks a link, which causes the router to change its current location. 2. Docusaurus preloads the next route's assets, while keeping displaying the current page's content. 3. The next route's assets have loaded. 4. The new location's route component gets rendered to DOM. -`onRouteUpdate` will be called at event (2), and `onRouteDidUpdate` will be called at (4). They both receive the previous location (which can be `null`, if this is the first screen). +`onRouteUpdate` will be called at event (2), and `onRouteDidUpdate` will be called at (4). They both receive the current location and the previous location (which can be `null`, if this is the first screen). `onRouteUpdate` can optionally return a "cleanup" callback, which will be called at (3). For example, if you want to display a progress bar, you can start a timeout in `onRouteUpdate`, and clear the timeout in the callback. (The classic theme already provides an `nprogress` integration this way.) From 4dda8a6f584c5d9c638a801964594efc3641ee6b Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 22:00:33 +0800 Subject: [PATCH 11/19] minor refactors --- packages/docusaurus-theme-classic/src/nprogress.css | 10 +++++++--- packages/docusaurus/src/client/PendingNavigation.tsx | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/nprogress.css b/packages/docusaurus-theme-classic/src/nprogress.css index c354979d2044..5e369a9c8b09 100644 --- a/packages/docusaurus-theme-classic/src/nprogress.css +++ b/packages/docusaurus-theme-classic/src/nprogress.css @@ -11,12 +11,16 @@ * https://github.com/rstacruz/nprogress/blob/master/nprogress.css */ +:root { + --docusaurus-progress-bar-color: var(--ifm-color-primary); +} + #nprogress { pointer-events: none; } #nprogress .bar { - background: var(--ifm-color-primary); + background: var(--docusaurus-progress-bar-color); position: fixed; z-index: 1031; top: 0; @@ -30,8 +34,8 @@ right: 0; width: 100px; height: 100%; - box-shadow: 0 0 10px var(--ifm-color-primary), - 0 0 5px var(--ifm-color-primary); + box-shadow: 0 0 10px var(--docusaurus-progress-bar-color), + 0 0 5px var(--docusaurus-progress-bar-color); opacity: 1; transform: rotate(3deg) translate(0, -4px); } diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index eaff76d68fe4..f3a87ed3e9cf 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -60,14 +60,16 @@ class PendingNavigation extends React.Component { // Load data while the old screen remains. preload(nextLocation.pathname) - .then(() => this.setState({nextRouteHasLoaded: true})) + .then(() => { + this.setState({nextRouteHasLoaded: true}); + this.routeUpdateCb?.(); + }) .catch((e) => console.warn(e)); return false; } override render(): JSX.Element { const {children, location} = this.props; - this.routeUpdateCb?.(); // Use a controlled to trick all descendants into rendering the old // location. return ( From f33826159d022bc4562d19a712f437f4e48d6e74 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 22:04:36 +0800 Subject: [PATCH 12/19] migrate analytics to onRouteDidUpdate --- packages/docusaurus-plugin-google-analytics/src/analytics.ts | 2 +- packages/docusaurus-plugin-google-gtag/src/gtag.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-google-analytics/src/analytics.ts b/packages/docusaurus-plugin-google-analytics/src/analytics.ts index e0196d77fbf1..ebf87f036041 100644 --- a/packages/docusaurus-plugin-google-analytics/src/analytics.ts +++ b/packages/docusaurus-plugin-google-analytics/src/analytics.ts @@ -13,7 +13,7 @@ export default (function analyticsModule() { } return { - onRouteUpdate({location}: {location: Location}) { + onRouteDidUpdate({location}: {location: Location}) { // Set page so that subsequent hits on this page are attributed // to this page. This is recommended for Single-page Applications. window.ga('set', 'page', location.pathname); diff --git a/packages/docusaurus-plugin-google-gtag/src/gtag.ts b/packages/docusaurus-plugin-google-gtag/src/gtag.ts index 77024f7bb5ed..e5d782bf3145 100644 --- a/packages/docusaurus-plugin-google-gtag/src/gtag.ts +++ b/packages/docusaurus-plugin-google-gtag/src/gtag.ts @@ -18,7 +18,7 @@ export default (function gtagModule() { .default as PluginOptions; return { - onRouteUpdate({location}: {location: Location}) { + onRouteDidUpdate({location}: {location: Location}) { // Always refer to the variable on window in case it gets overridden // elsewhere. window.gtag('config', trackingID, { From 409c29f8d3412cc113be8c7a5a15220c646ac504 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 22:06:27 +0800 Subject: [PATCH 13/19] docs --- website/docs/advanced/client.md | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/website/docs/advanced/client.md b/website/docs/advanced/client.md index ac87fcd8371e..9fea051ded45 100644 --- a/website/docs/advanced/client.md +++ b/website/docs/advanced/client.md @@ -136,16 +136,10 @@ For every route transition, there will be several important timings: Note that the new page's DOM is only available during event (4). If you need to manipulate the new page's DOM, you'll likely want to use `onRouteDidUpdate`, which will be fired as soon as the DOM on the new page has mounted. -```ts title="myClientModule.ts" +```js title="myClientModule.js" import type {Location} from 'history'; -export function onRouteDidUpdate({ - location, - previousLocation, -}: { - location: Location; - previousLocation: Location | null; -}) { +export function onRouteDidUpdate({location, previousLocation}) { // Don't execute if we are still on the same page; the lifecycle may be fired // because the hash changes (e.g. when navigating between headings) if (location.pathname !== previousLocation?.pathname) { @@ -156,13 +150,7 @@ export function onRouteDidUpdate({ } } -export function onRouteUpdate({ - location, - previousLocation, -}: { - location: Location; - previousLocation: Location | null; -}): void { +export function onRouteUpdate({location, previousLocation}) { if (location.pathname !== previousLocation?.pathname) { const progressBarTimeout = window.setTimeout(() => { nprogress.start(); @@ -173,6 +161,22 @@ export function onRouteUpdate({ } ``` +Or, if you are using TypeScript and you want to leverage contextual typing: + +```ts title="myClientModule.js" +import type {ClientModule} from '@docusaurus/types'; + +const module: ClientModule = { + onRouteUpdate({location, previousLocation}) { + // ... + }, + onRouteDidUpdate({location, previousLocation}) { + // ... + }, +}; +export default module; +``` + :::tip Prefer using React Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.md) instead. From de9d03e74cec5e4b2bdcbff5d0a01a11f8c952a1 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 28 Apr 2022 22:44:33 +0800 Subject: [PATCH 14/19] fix --- packages/docusaurus/src/client/PendingNavigation.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index f3a87ed3e9cf..99b5ae575a1d 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -22,7 +22,7 @@ type State = { class PendingNavigation extends React.Component { private previousLocation: Location | null; - private routeUpdateCb: (() => void) | undefined; + private routeUpdateCleanupCb: (() => void) | undefined; private clientLifecyclesDispatcher: React.RefObject>; constructor(props: Props) { @@ -30,7 +30,7 @@ class PendingNavigation extends React.Component { // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; - this.routeUpdateCb = undefined; + this.routeUpdateCleanupCb = undefined; this.clientLifecyclesDispatcher = React.createRef(); this.state = { nextRouteHasLoaded: true, @@ -52,7 +52,7 @@ class PendingNavigation extends React.Component { // Save the location first. this.previousLocation = this.props.location; this.setState({nextRouteHasLoaded: false}); - this.routeUpdateCb = + this.routeUpdateCleanupCb = this.clientLifecyclesDispatcher.current?.onRouteUpdate({ previousLocation: this.previousLocation, location: nextLocation, @@ -61,8 +61,8 @@ class PendingNavigation extends React.Component { // Load data while the old screen remains. preload(nextLocation.pathname) .then(() => { + this.routeUpdateCleanupCb?.(); this.setState({nextRouteHasLoaded: true}); - this.routeUpdateCb?.(); }) .catch((e) => console.warn(e)); return false; From 8d31081d9174618713814accc3ad032c5788247f Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 29 Apr 2022 18:28:44 +0800 Subject: [PATCH 15/19] fire onRouteUpdate during init --- .../src/client/ClientLifecyclesDispatcher.tsx | 34 +++++++------------ .../src/client/PendingNavigation.tsx | 24 ++++++------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx index 5bb771ce188e..7c797af092d7 100644 --- a/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx +++ b/packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx @@ -5,16 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import React, { - useImperativeHandle, - useLayoutEffect, - type ReactElement, -} from 'react'; +import {useLayoutEffect, type ReactElement} from 'react'; import clientModules from '@generated/client-modules'; import type {ClientModule} from '@docusaurus/types'; import type {Location} from 'history'; -function dispatchLifecycleAction( +export function dispatchLifecycleAction( lifecycleAction: K, ...args: Parameters> ): () => void { @@ -31,18 +27,15 @@ function dispatchLifecycleAction( return () => callbacks.forEach((cb) => cb?.()); } -function ClientLifecyclesDispatcher( - { - children, - location, - previousLocation, - }: { - children: ReactElement; - location: Location; - previousLocation: Location | null; - }, - ref: React.ForwardedRef, -): JSX.Element { +function ClientLifecyclesDispatcher({ + children, + location, + previousLocation, +}: { + children: ReactElement; + location: Location; + previousLocation: Location | null; +}): JSX.Element { useLayoutEffect(() => { if (previousLocation !== location) { const {hash} = location; @@ -56,10 +49,7 @@ function ClientLifecyclesDispatcher( dispatchLifecycleAction('onRouteDidUpdate', {previousLocation, location}); } }, [previousLocation, location]); - useImperativeHandle(ref, () => ({ - onRouteUpdate: (args) => dispatchLifecycleAction('onRouteUpdate', args), - })); return children; } -export default React.forwardRef(ClientLifecyclesDispatcher); +export default ClientLifecyclesDispatcher; diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 99b5ae575a1d..555a39d7839b 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -7,10 +7,11 @@ import React from 'react'; import {Route} from 'react-router-dom'; -import ClientLifecyclesDispatcher from './ClientLifecyclesDispatcher'; +import ClientLifecyclesDispatcher, { + dispatchLifecycleAction, +} from './ClientLifecyclesDispatcher'; import preload from './preload'; import type {Location} from 'history'; -import type {ClientModule} from '@docusaurus/types'; type Props = { readonly location: Location; @@ -22,16 +23,17 @@ type State = { class PendingNavigation extends React.Component { private previousLocation: Location | null; - private routeUpdateCleanupCb: (() => void) | undefined; - private clientLifecyclesDispatcher: React.RefObject>; + private routeUpdateCleanupCb: () => void; constructor(props: Props) { super(props); // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; - this.routeUpdateCleanupCb = undefined; - this.clientLifecyclesDispatcher = React.createRef(); + this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', { + previousLocation: null, + location: this.props.location, + })!; this.state = { nextRouteHasLoaded: true, }; @@ -52,11 +54,10 @@ class PendingNavigation extends React.Component { // Save the location first. this.previousLocation = this.props.location; this.setState({nextRouteHasLoaded: false}); - this.routeUpdateCleanupCb = - this.clientLifecyclesDispatcher.current?.onRouteUpdate({ - previousLocation: this.previousLocation, - location: nextLocation, - }) || undefined; + this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', { + previousLocation: this.previousLocation, + location: nextLocation, + })!; // Load data while the old screen remains. preload(nextLocation.pathname) @@ -74,7 +75,6 @@ class PendingNavigation extends React.Component { // location. return ( children} /> From 285e8307eccb14dbb1d685651a4059d21b8deff2 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 29 Apr 2022 19:12:26 +0800 Subject: [PATCH 16/19] do not fire on SSG --- packages/docusaurus/src/client/PendingNavigation.tsx | 11 +++++++---- website/docs/advanced/client.md | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index 555a39d7839b..a60825cc5c7e 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -10,6 +10,7 @@ import {Route} from 'react-router-dom'; import ClientLifecyclesDispatcher, { dispatchLifecycleAction, } from './ClientLifecyclesDispatcher'; +import ExecutionEnvironment from './exports/ExecutionEnvironment'; import preload from './preload'; import type {Location} from 'history'; @@ -30,10 +31,12 @@ class PendingNavigation extends React.Component { // previousLocation doesn't affect rendering, hence not stored in state. this.previousLocation = null; - this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', { - previousLocation: null, - location: this.props.location, - })!; + this.routeUpdateCleanupCb = ExecutionEnvironment.canUseDOM + ? dispatchLifecycleAction('onRouteUpdate', { + previousLocation: null, + location: this.props.location, + })! + : () => {}; this.state = { nextRouteHasLoaded: true, }; diff --git a/website/docs/advanced/client.md b/website/docs/advanced/client.md index 9fea051ded45..fe09876169e0 100644 --- a/website/docs/advanced/client.md +++ b/website/docs/advanced/client.md @@ -177,6 +177,8 @@ const module: ClientModule = { export default module; ``` +Both lifecycles will fire on first render, but they will not fire on server-side, so you can safely access browser globals in them. + :::tip Prefer using React Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.md) instead. From 5746f9522532bfacb5cfbb76a67221e4cdd90556 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 29 Apr 2022 20:05:39 +0800 Subject: [PATCH 17/19] minor refactor --- .../docusaurus-theme-classic/src/nprogress.ts | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/nprogress.ts b/packages/docusaurus-theme-classic/src/nprogress.ts index 4202032b33a5..9db730401d2e 100644 --- a/packages/docusaurus-theme-classic/src/nprogress.ts +++ b/packages/docusaurus-theme-classic/src/nprogress.ts @@ -7,28 +7,25 @@ import nprogress from 'nprogress'; import './nprogress.css'; -import type {Location} from 'history'; +import type {ClientModule} from '@docusaurus/types'; nprogress.configure({showSpinner: false}); const delay = 200; -export function onRouteUpdate({ - location, - previousLocation, -}: { - location: Location; - previousLocation: Location | null; -}): (() => void) | undefined { - if (location.pathname !== previousLocation?.pathname) { - const progressBarTimeout = window.setTimeout(() => { - nprogress.start(); - }, delay); - return () => window.clearTimeout(progressBarTimeout); - } - return undefined; -} +const clientModule: ClientModule = { + onRouteUpdate({location, previousLocation}) { + if (location.pathname !== previousLocation?.pathname) { + const progressBarTimeout = window.setTimeout(() => { + nprogress.start(); + }, delay); + return () => window.clearTimeout(progressBarTimeout); + } + return undefined; + }, + onRouteDidUpdate() { + nprogress.done(); + }, +}; -export function onRouteDidUpdate(): void { - nprogress.done(); -} +export default clientModule; From 9286297deed9af802b56670787b18050602b25d0 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 29 Apr 2022 20:16:15 +0800 Subject: [PATCH 18/19] fix... --- packages/docusaurus-theme-classic/src/nprogress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-classic/src/nprogress.ts b/packages/docusaurus-theme-classic/src/nprogress.ts index 9db730401d2e..7e5103b98ef6 100644 --- a/packages/docusaurus-theme-classic/src/nprogress.ts +++ b/packages/docusaurus-theme-classic/src/nprogress.ts @@ -15,7 +15,7 @@ const delay = 200; const clientModule: ClientModule = { onRouteUpdate({location, previousLocation}) { - if (location.pathname !== previousLocation?.pathname) { + if (previousLocation && location.pathname !== previousLocation.pathname) { const progressBarTimeout = window.setTimeout(() => { nprogress.start(); }, delay); From c244bb1347cd92dda976725f2bd9d9f423c76fa1 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Fri, 29 Apr 2022 20:32:59 +0800 Subject: [PATCH 19/19] fix analytics --- .../src/analytics.ts | 20 +++++++------- .../docusaurus-plugin-google-gtag/src/gtag.ts | 26 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/docusaurus-plugin-google-analytics/src/analytics.ts b/packages/docusaurus-plugin-google-analytics/src/analytics.ts index ebf87f036041..82e957701c3a 100644 --- a/packages/docusaurus-plugin-google-analytics/src/analytics.ts +++ b/packages/docusaurus-plugin-google-analytics/src/analytics.ts @@ -5,21 +5,19 @@ * LICENSE file in the root directory of this source tree. */ -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import type {ClientModule} from '@docusaurus/types'; -export default (function analyticsModule() { - if (!ExecutionEnvironment.canUseDOM) { - return null; - } - - return { - onRouteDidUpdate({location}: {location: Location}) { +const clientModule: ClientModule = { + onRouteDidUpdate({location, previousLocation}) { + if (previousLocation && location.pathname !== previousLocation.pathname) { // Set page so that subsequent hits on this page are attributed // to this page. This is recommended for Single-page Applications. window.ga('set', 'page', location.pathname); // Always refer to the variable on window in-case it gets // overridden elsewhere. window.ga('send', 'pageview'); - }, - }; -})(); + } + }, +}; + +export default clientModule; diff --git a/packages/docusaurus-plugin-google-gtag/src/gtag.ts b/packages/docusaurus-plugin-google-gtag/src/gtag.ts index e5d782bf3145..0beaac7f0a07 100644 --- a/packages/docusaurus-plugin-google-gtag/src/gtag.ts +++ b/packages/docusaurus-plugin-google-gtag/src/gtag.ts @@ -5,20 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import globalData from '@generated/globalData'; import type {PluginOptions} from '@docusaurus/plugin-google-gtag'; +import type {ClientModule} from '@docusaurus/types'; -export default (function gtagModule() { - if (!ExecutionEnvironment.canUseDOM) { - return null; - } +const {trackingID} = globalData['docusaurus-plugin-google-gtag']! + .default as PluginOptions; - const {trackingID} = globalData['docusaurus-plugin-google-gtag']! - .default as PluginOptions; - - return { - onRouteDidUpdate({location}: {location: Location}) { +const clientModule: ClientModule = { + onRouteDidUpdate({location, previousLocation}) { + if (previousLocation && location.pathname !== previousLocation.pathname) { // Always refer to the variable on window in case it gets overridden // elsewhere. window.gtag('config', trackingID, { @@ -27,9 +23,11 @@ export default (function gtagModule() { }); window.gtag('event', 'page_view', { page_title: document.title, - page_location: location.href, + page_location: window.location.href, page_path: location.pathname, }); - }, - }; -})(); + } + }, +}; + +export default clientModule;