Client architecture
Theme aliases
A theme works by exporting a set of components, e.g. Navbar
, Layout
, Footer
, to render the data passed down from plugins. Docusaurus and users use these components by importing them using the @theme
webpack alias:
import Navbar from '@theme/Navbar';
The alias @theme
can refer to a few directories, in the following priority:
- A user's
website/src/theme
directory, which is a special directory that has the higher precedence. - A Docusaurus theme package's
theme
directory. - Fallback components provided by Docusaurus core (usually not needed).
This is called a layered architecture: a higher-priority layer providing the component would shadow a lower-priority layer, making swizzling possible. Given the following structure:
website
├── node_modules
│ └── @docusaurus/theme-classic
│ └── theme
│ └── Navbar.js
└── src
└── theme
└── Navbar.js
website/src/theme/Navbar.js
takes precedence whenever @theme/Navbar
is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target @theme/Navbar
is pointing to!
We already talked about how the "userland theme" in src/theme
can re-use a theme component through the @theme-original
alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the @theme-init
import.
Here's an example of using this feature to enhance the default theme CodeBlock
component with a react-live
playground feature.
import InitialCodeBlock from '@theme-init/CodeBlock';
import React from 'react';
export default function CodeBlock(props) {
return props.live ? (
<ReactLivePlayground {...props} />
) : (
<InitialCodeBlock {...props} />
);
}
Check the code of @docusaurus/theme-live-codeblock
for details.
Unless you want to publish a re-usable "theme enhancer" (like @docusaurus/theme-live-codeblock
), you likely don't need @theme-init
.
It can be quite hard to wrap your mind around these aliases. Let's imagine the following case with a super convoluted setup with three themes/plugins and the site itself all trying to define the same component. Internally, Docusaurus loads these themes as a "stack".
+-------------------------------------------------+
| `website/src/theme/CodeBlock.js` | <-- `@theme/CodeBlock` always points to the top
+-------------------------------------------------+
| `theme-live-codeblock/theme/CodeBlock/index.js` | <-- `@theme-original/CodeBlock` points to the topmost non-swizzled component
+-------------------------------------------------+
| `plugin-awesome-codeblock/theme/CodeBlock.js` |
+-------------------------------------------------+
| `theme-classic/theme/CodeBlock/index.js` | <-- `@theme-init/CodeBlock` always points to the bottom
+-------------------------------------------------+
The components in this "stack" are pushed in the order of preset plugins > preset themes > plugins > themes > site
, so the swizzled component in website/src/theme
always comes out on top because it's loaded last.
@theme/*
always points to the topmost component—when CodeBlock
is swizzled, all other components requesting @theme/CodeBlock
receive the swizzled version.
@theme-original/*
always points to the topmost non-swizzled component. That's why you can import @theme-original/CodeBlock
in the swizzled component—it points to the next one in the "component stack", a theme-provided one. Plugin authors should not try to use this because your component could be the topmost component and cause a self-import.
@theme-init/*
always points to the bottommost component—usually, this comes from the theme or plugin that first provides this component. Individual plugins / themes trying to enhance code block can safely use @theme-init/CodeBlock
to get its basic version. Site creators should generally not use this because you likely want to enhance the topmost instead of the bottommost component. It's also possible that the @theme-init/CodeBlock
alias does not exist at all—Docusaurus only creates it when it points to a different one from @theme-original/CodeBlock
, i.e. when it's provided by more than one theme. We don't waste aliases!
Client modules
Client modules are part of your site's bundle, just like theme components. However, they are usually side-effect-ful. Client modules are anything that can be import
ed by Webpack—CSS, JS, etc. JS scripts usually work on the global context, like registering event listeners, creating global variables...
These modules are imported globally before React even renders the initial UI.
// How it works under the hood
import '@generated/client-modules';
Plugins and sites can both declare client modules, through getClientModules
and siteConfig.clientModules
, respectively.
Client modules are called during server-side rendering as well, so remember to check the execution environment before accessing client-side globals.
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
// As soon as the site loads in the browser, register a global event listener
window.addEventListener('keydown', (e) => {
if (e.code === 'Period') {
location.assign(location.href.replace('.com', '.dev'));
}
});
}
CSS stylesheets imported as client modules are global.
/* This stylesheet is global. */
.globalSelector {
color: red;
}
Client module lifecycles
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 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 timings:
- The user clicks a link, which causes the router to change its current location.
- Docusaurus preloads the next route's assets, while keeping displaying the current page's content.
- The next route's assets have loaded.
- 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 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.)
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.
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) {
const title = document.getElementsByTagName('h1')[0];
if (title) {
title.innerText += '❤️';
}
}
}
export function onRouteUpdate({location, previousLocation}) {
if (location.pathname !== previousLocation?.pathname) {
const progressBarTimeout = window.setTimeout(() => {
nprogress.start();
}, delay);
return () => window.clearTimeout(progressBarTimeout);
}
return undefined;
}
Or, if you are using TypeScript and you want to leverage contextual typing:
import type {ClientModule} from '@docusaurus/types';
const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
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.
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 instead.