From 628995e14e60fcddb15144638e3c47f36656839f Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Fri, 17 Feb 2023 19:58:54 +0000 Subject: [PATCH] docs(advanced-guides): explaining the difference between "hydration completion" versus "actually being in the browser environment" in `useIsBrowser()` hook --- website/docs/advanced/ssg.mdx | 77 +++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/website/docs/advanced/ssg.mdx b/website/docs/advanced/ssg.mdx index 7fd0724ece..4caf0dc681 100644 --- a/website/docs/advanced/ssg.mdx +++ b/website/docs/advanced/ssg.mdx @@ -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 {location}; -} +}; ``` +#### 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 {someParam}; + // highlight-end +}; +``` + +Adding `useIsBrowser()` checks to derived values will have no effect. Wrapping the `` with `` 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 {someParam}; +}; + +// highlight-start +const MyComponentParent = () => { + const isBrowser = useIsBrowser(); + + if (!isBrowser) { + return null; + } + + return ; +}; +// 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 `` 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.