-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
948ef8e
628995e
47cc3e6
67c3c20
6206a99
d53136d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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` | ||
|
||
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 />; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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. | ||
|
There was a problem hiding this comment.
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)