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

docs(core): explaining the difference between "hydration completion" versus "actually being in the browser environment" in useIsBrowser hook #8679

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
77 changes: 74 additions & 3 deletions website/docs/advanced/ssg.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,89 @@ While you may expect that `BrowserOnly` hides away the children during server-si

### `useIsBrowser` {#useisbrowser}

You can also use the `useIsBrowser()` hook to test if the component is currently in a browser environment. It returns `false` in SSR and `true` is CSR, after first client render. Use this hook if you only need to perform certain conditional operations on client-side, but not render an entirely different UI.
Returns `true` when the React app has successfully hydrated in the browser.

:::caution

Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic.

The first client-side render output (in the browser) **must be exactly the same** as the server-side render output (Node.js). Not following this rule can lead to unexpected hydration behaviors, as described in [The Perils of Rehydration](https://www.joshwcomeau.com/react/the-perils-of-rehydration/).

:::

Usage example:

```jsx
import React from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const MyComponent = () => {
// highlight-start
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
// highlight-end
return <span>{location}</span>;
}
};
```

#### A caveat to know when using `useIsBrowser`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this duplicated whole paragraph looks overkill to me. We probably don't want twice the same doc fragment like that.

Also these hydration problems are a bit out of the scope of Docusaurus, they also affect any other framework using hydration. You should normally not use any value from window in the render path, including in useState initializers (tried to explain a bit here: https://twitter.com/sebastienlorber/status/1615329010761842689)


Because it does not do `typeof windows !== 'undefined'` check but rather checks if the React app has successfully hydrated, the following code will not work as intended:

```jsx
import React from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

const MyComponent = () => {
// highlight-start
const isBrowser = useIsBrowser();
const url = isBrowser ? new URL(window.location.href) : undefined;
const someQueryParam = url?.searchParams.get('someParam');
const [someParam, setSomeParam] = useState(someQueryParam || 'fallbackValue');

// renders fallbackValue instead of the value of someParam query parameter
// because the component has already rendered but hydration has not completed
// useState references the fallbackValue
return <span>{someParam}</span>;
// highlight-end
};
```

Adding `useIsBrowser()` checks to derived values will have no effect. Wrapping the `<span>` with `<BrowserOnly>` will also have no effect. To have `useState` reference the correct value, which is the value of the `someParam` query parameter, `MyComponent`'s first render should actually happen after `useIsBrowser` returns true. Because you cannot have if statements inside the component before any hooks, you need to resort to doing `useIsBrowser()` in the parent component as such:

```jsx
import React, {useState} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

const MyComponent = () => {
const isBrowser = useIsBrowser();
const url = isBrowser ? new URL(window.location.href) : undefined;
const someQueryParam = url?.searchParams.get('someParam');
const [someParam, setSomeParam] = useState(someQueryParam || 'fallbackValue');

return <span>{someParam}</span>;
};

// highlight-start
const MyComponentParent = () => {
const isBrowser = useIsBrowser();

if (!isBrowser) {
return null;
}

return <MyComponent />;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use in the parent component, but you still have to split this into 2 components

};
// highlight-end

export default MyComponentParent;
```

There are a couple more alternative solutions to this problem. However all of them require adding checks in **the parent component**:

1. You can wrap `<MyComponent />` with [`BrowserOnly`](../docusaurus-core.mdx#browseronly)
2. You can use `canUseDOM` from [`ExecutionEnvironment`](../docusaurus-core.mdx#executionenvironment) and `return null` when `canUseDOM` is `false`

### `useEffect` {#useeffect}

Lastly, you can put your logic in `useEffect()` to delay its execution until after first CSR. This is most appropriate if you are only performing side-effects but don't _get_ data from the client state.
Expand Down
58 changes: 58 additions & 0 deletions website/docs/docusaurus-core.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,64 @@ const MyComponent = () => {
};
```

#### A caveat to know when using `useIsBrowser`

Because it does not do `typeof windows !== 'undefined'` check but rather checks if the React app has successfully hydrated, the following code will not work as intended:

```jsx
import React from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

const MyComponent = () => {
// highlight-start
const isBrowser = useIsBrowser();
const url = isBrowser ? new URL(window.location.href) : undefined;
const someQueryParam = url?.searchParams.get('someParam');
const [someParam, setSomeParam] = useState(someQueryParam || 'fallbackValue');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should rather not read the querystring or any client-side value in the render path in any SSR React app (not just Docusaurus). Similarly, this can lead to hydration mismatches, not to mention that the value is not updated on navigation.

Values derived from querystring should also not use memo or whatever but effects instead.

useSyncExternalStore is a great way to read the querystring IMHO, see https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api


// renders fallbackValue instead of the value of someParam query parameter
// because the component has already rendered but hydration has not completed
// useState references the fallbackValue
return <span>{someParam}</span>;
// highlight-end
};
```

Adding `useIsBrowser()` checks to derived values will have no effect. Wrapping the `<span>` with `<BrowserOnly>` will also have no effect. To have `useState` reference the correct value, which is the value of the `someParam` query parameter, `MyComponent`'s first render should actually happen after `useIsBrowser` returns true. Because you cannot have if statements inside the component before any hooks, you need to resort to doing `useIsBrowser()` in the parent component as such:

```jsx
import React, {useState} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

const MyComponent = () => {
const isBrowser = useIsBrowser();
const url = isBrowser ? new URL(window.location.href) : undefined;
const someQueryParam = url?.searchParams.get('someParam');
const [someParam, setSomeParam] = useState(someQueryParam || 'fallbackValue');

return <span>{someParam}</span>;
};

// highlight-start
const MyComponentParent = () => {
const isBrowser = useIsBrowser();

if (!isBrowser) {
return null;
}

return <MyComponent />;
};
// highlight-end

export default MyComponentParent;
```

There are a couple more alternative solutions to this problem. However all of them require adding checks in **the parent component**:

1. You can wrap `<MyComponent />` with [`BrowserOnly`](./docusaurus-core.mdx#browseronly)
2. You can use `canUseDOM` from [`ExecutionEnvironment`](./docusaurus-core.mdx#executionenvironment) and `return null` when `canUseDOM` is `false`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no you shouldn't do that, otherwise it leads to an hydration mismatch:

  • server: return null
  • client first render: return JSX

it should be the same on server/first client render. It's not just JSX1 vs JSX2, but also null vs JSX.


### `useBaseUrl` {#useBaseUrl}

React hook to prepend your site `baseUrl` to a string.
Expand Down