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
93 changes: 89 additions & 4 deletions website/docs/advanced/ssg.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,103 @@ 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 because `window` may be defined but hydration may not necessarily have been completed yet.

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 // useEffect // useState,
'react';
import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const isFetchingLocationMessage = 'fetching location...';

const MyComponent = () => {
// highlight-start
// Recommended
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : isFetchingLocationMessage;

// Not Recommended
// using typeof window !== 'undefined' will still work in this example
// but not recommended as it may cause issues depending on your business logic
const isWindowDefined = typeof window !== 'undefined';
const thisWillWorkButNotRecommended = isWindowDefined
? window.location.href
: isFetchingLocationMessage;

// highlight-end
return (
<div>
{/* Recommended */}
{location}

{/* Not Recommended */}
{thisWillWorkButNotRecommended}
</div>
);
};
```

:::caution If your business logic in the component relies on browser specifics to be functional at all, we recommend using [`<BrowserOnly>`](../docusaurus-core.mdx#browseronly). The following example will cause business logic issues when used with `useIsBrowser`:

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

const isFetchingLocationMessage = 'fetching location...';

const MyComponent = () => {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
const location = isBrowser ? window.location.href : isFetchingLocationMessage;
// highlight-start
const [isFetchingLocation, setIsFetchingLocation] = useState(
location === isFetchingLocationMessage,
);

// highlight-end
return (
<div>
{/*
This will always print true and will not update.
Component already rendered once and useState referenced the initial value as `true`
*/}
{isFetchingLocation}
</div>
);
};
```

To solve this, you can add a `useEffect` to run when hydration has completed:

```js
useEffect(() => {
setIsFetchingLocation(location === isFetchingLocationMessage);
}, [isBrowser]);
```

Or use [`<BrowserOnly>`](../docusaurus-core.mdx#browseronly):

```
const ParentComponent = () => {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => <MyComponent />}
</BrowserOnly>
)
}
```

:::

### `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
19 changes: 17 additions & 2 deletions website/docs/docusaurus-core.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ 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.
Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic because `window` may be defined but hydration may not necessarily have been completed yet.

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/).

Expand All @@ -427,9 +427,24 @@ import useIsBrowser from '@docusaurus/useIsBrowser';

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

// Not Recommended
// using typeof window !== 'undefined' will lead to mismatching render output
const isWindowDefined = typeof window !== 'undefined';
// highlight-end
return <div>{isBrowser ? 'Client' : 'Server'}</div>;
return (
<div>
{/* Recommended */}
{isBrowser ? 'Client (hydration completed)' : 'Server'}

{/* Not Recommended */}
{isWindowDefined
? 'Client (hydration NOT completed, will mismatch)'
: 'Server'}
</div>
);
};
```

Expand Down