From 948ef8e4f332d272632d2468cdeb0916c53b8cc9 Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Fri, 17 Feb 2023 19:57:11 +0000 Subject: [PATCH 1/6] docs(core): explaining the difference between "hydration completion" versus "actually being in the browser environment" in `useIsBrowser()` hook --- website/docs/docusaurus-core.mdx | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index 7e6615d8841e..e18459b18dd9 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -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'); + + // 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` + ### `useBaseUrl` {#useBaseUrl} React hook to prepend your site `baseUrl` to a string. From 628995e14e60fcddb15144638e3c47f36656839f Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Fri, 17 Feb 2023 19:58:54 +0000 Subject: [PATCH 2/6] 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 7fd0724ece35..4caf0dc681a1 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. From 47cc3e62e8c0133c219c0bb7d90ee018bfd09dcb Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Fri, 17 Feb 2023 20:44:54 +0000 Subject: [PATCH 3/6] docs(core): fixing broken links in `useIsBrowser` section's caveat explanation about "hydration completion" versus "is in the browser environment" --- website/docs/docusaurus-core.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index e18459b18dd9..251f7d0fc30b 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -488,8 +488,8 @@ 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` +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` ### `useBaseUrl` {#useBaseUrl} From 67c3c20db0abc7ef1f54f237944f680bdb2e9a90 Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Thu, 20 Apr 2023 09:32:02 +0300 Subject: [PATCH 4/6] docs(core): removing the newly added explanations for `useIsBrowser` as they are not in the scope of only Docusaurus but all the React SSR frameworks and extending the existing example to include a bit more context --- website/docs/docusaurus-core.mdx | 73 +++++++------------------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index 251f7d0fc30b..f7847cace2e4 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -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/). @@ -427,70 +427,27 @@ import useIsBrowser from '@docusaurus/useIsBrowser'; const MyComponent = () => { // highlight-start + // Recommended const isBrowser = useIsBrowser(); - // highlight-end - return
{isBrowser ? 'Client' : 'Server'}
; -}; -``` - -#### 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}; + // Not Recommended + // using typeof window !== 'undefined' will lead to mismatching render output + const isWindowDefined = typeof window !== 'undefined'; // 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 ( +
+ {/* Recommended */} + {isBrowser ? 'Client (hydration completed)' : 'Server'} - return ; + {/* Not Recommended */} + {isWindowDefined + ? 'Client (hydration NOT completed, will mismatch)' + : 'Server'} +
+ ); }; -// 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` - ### `useBaseUrl` {#useBaseUrl} React hook to prepend your site `baseUrl` to a string. From 6206a99c10302d72081905e7471e821f6d137b74 Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Thu, 20 Apr 2023 10:08:07 +0300 Subject: [PATCH 5/6] core(advanced-guides): removing the newly added explanations for `useIsBrowser` as they are not in the scope of only Docusaurus but all the React SSR frameworks and extending the existing example to include a bit more context --- website/docs/advanced/ssg.mdx | 100 ++++++++++++++-------------------- 1 file changed, 41 insertions(+), 59 deletions(-) diff --git a/website/docs/advanced/ssg.mdx b/website/docs/advanced/ssg.mdx index 4caf0dc681a1..4f19fc2f3284 100644 --- a/website/docs/advanced/ssg.mdx +++ b/website/docs/advanced/ssg.mdx @@ -181,7 +181,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/). @@ -190,76 +190,58 @@ The first client-side render output (in the browser) **must be exactly the same* Usage example: ```jsx -import React from 'react'; +import React from // useState, +// useEffect +'react'; import useIsBrowser from '@docusaurus/useIsBrowser'; -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 isFetchingLocationMessage = 'fetching location...'; const MyComponent = () => { // highlight-start + // Recommended 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}; -}; + 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; + + /* + const [isFetchingLocation, setIsFetchingLocation] = useState( + location === isFetchingLocationMessage + ) + // Please note that `isFetchingLocation` initial value will be `true` + // even though window is actually defined + // because component has actually rendered once before hydration + // subsequent renders will not update the useState + // and may cause issues with business logic + + // Do this instead + const [isFetchingLocation, setIsFetchingLocation] = useState(true) -// highlight-start -const MyComponentParent = () => { - const isBrowser = useIsBrowser(); + useEffect(() => { + setIsFetchingLocation(location === isFetchingLocationMessage) + }, [isBrowser]) + */ - if (!isBrowser) { - return null; - } + // highlight-end + return ( +
+ {/* Recommended */} + {location} - return ; + {/* Not Recommended */} + {thisWillWorkButNotRecommended} +
+ ); }; -// 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. From d53136d59c7d04ddc75fba426daa86113c1759d3 Mon Sep 17 00:00:00 2001 From: Sercan AKMAN Date: Thu, 20 Apr 2023 10:39:05 +0300 Subject: [PATCH 6/6] docs(advanced-guides): adding a bit more context to SSG `useIsBrowser` documentation to promote use of --- website/docs/advanced/ssg.mdx | 72 +++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/website/docs/advanced/ssg.mdx b/website/docs/advanced/ssg.mdx index 4f19fc2f3284..3c7a993d65e3 100644 --- a/website/docs/advanced/ssg.mdx +++ b/website/docs/advanced/ssg.mdx @@ -190,8 +190,7 @@ The first client-side render output (in the browser) **must be exactly the same* Usage example: ```jsx -import React from // useState, -// useEffect +import React from // useEffect // useState, 'react'; import useIsBrowser from '@docusaurus/useIsBrowser'; @@ -211,24 +210,6 @@ const MyComponent = () => { ? window.location.href : isFetchingLocationMessage; - /* - const [isFetchingLocation, setIsFetchingLocation] = useState( - location === isFetchingLocationMessage - ) - // Please note that `isFetchingLocation` initial value will be `true` - // even though window is actually defined - // because component has actually rendered once before hydration - // subsequent renders will not update the useState - // and may cause issues with business logic - - // Do this instead - const [isFetchingLocation, setIsFetchingLocation] = useState(true) - - useEffect(() => { - setIsFetchingLocation(location === isFetchingLocationMessage) - }, [isBrowser]) - */ - // highlight-end return (
@@ -242,6 +223,57 @@ const MyComponent = () => { }; ``` +:::caution If your business logic in the component relies on browser specifics to be functional at all, we recommend using [``](../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 : isFetchingLocationMessage; + // highlight-start + const [isFetchingLocation, setIsFetchingLocation] = useState( + location === isFetchingLocationMessage, + ); + + // highlight-end + return ( +
+ {/* + This will always print true and will not update. + Component already rendered once and useState referenced the initial value as `true` + */} + {isFetchingLocation} +
+ ); +}; +``` + +To solve this, you can add a `useEffect` to run when hydration has completed: + +```js +useEffect(() => { + setIsFetchingLocation(location === isFetchingLocationMessage); +}, [isBrowser]); +``` + +Or use [``](../docusaurus-core.mdx#browseronly): + +``` +const ParentComponent = () => { + return ( + Loading...
}> + {() => } +
+ ) +} +``` + +::: + ### `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.