-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Description
Have you read the Contributing Guidelines on issues?
- I have read the Contributing Guidelines on issues.
Description
Hello,
Thanks for the amazing library.
The comment here states that useIsBrowser
returns true only if the first hydration has completed.
But opening this Stackblits Example and checking the console, you see logs printed for the below code:
const isBrowser = useIsBrowser();
const traditionalIsBrowser = typeof window !== 'undefined';
const testUrl = isBrowser ? new URL(window.location.href) : undefined;
console.log('Has completed first hydration?', isBrowser);
console.log('Are we actually on browser?', traditionalIsBrowser);
console.log('Test URL:', testUrl);
Outputs before hydration, on first render:
Has completed first hydration? false
Are we actually on browser? true
Test URL: undefined
Outputs after hydration:
Has completed first hydration? true
Are we actually on browser? true
Test URL: URL {origin: 'https://githubev3fmt-xvco--3000.local-credentialless.webcontainer.io', protocol: 'https:', username: '', password: '', host: 'githubev3fmt-xvco--3000.local-credentialless.webcontainer.io', …}
So the name of useIsBrowser
actually should be along the lines of useHasHydrationCompleted
.
When there are a lot of window
references in a component, and you try to build the application, you get this error:
It looks like you are using code that should run on the client-side only.
To get around it, try using `<BrowserOnly>` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or `ExecutionEnvironment` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
It might also require to wrap your client code in `useEffect` hook and/or import a third-party library dynamically (if any).
ReferenceError: window is not defined
So naturally your first instinct as a developer is to add <BrowserOnly>
and useIsBrowser
to everywhere you see window
references unless you also read about ExecutionEnvironment, which I did much much later after I've already wasted tons of time.
I did not read ExecutionEnvironment up until creating this ticket because the ExecutionEnvironment did not sound like something I might need (I am burnt out enough to skip reading about it even though the error basically tells you to read it) however it's name is also misleading, I understand that it has canUseDOM
for such use case but maybe it should rather be something along the lines of CanUse
with:
CanUse.DOM
CanUse.IntersectionObserver
CanUse.Viewport
What ExecutionEnvironment.canUseDOM
does is actually what useIsBrowser
should be doing therefore maybe these can be extracted into several hooks with appropriate names.
As a rule of thumb while naming things, I tend to name them according to what they are doing. So these would be:
- useIsWindowDefined
- useAreEventsInWindow
- useIsIntersectionObserverInWindow
- useIsScreenInWindow
When you have code like this:
const testUrl = new URL(window.location.href)
,
you end up making it (that is also what useIsBrowser documentation tells you to do)
const testUrl = isBrowser ? new URL(window.location.href) : undefined
But because you just added isBrowser
and make it purposefully return something else such asundefined
, the code dependent starts acting as it should not. Then if you have tons of code that relies on testUrl
and you suddenly return undefined
with it, you end up modifying the entire stack of code to compensate for testUrl
being undefined.
Now on the same Stackblitz example change the URL to include ?params=test123
. You will see in the page under "Docusaurus Tutorial - 5min" area, the value rendered is "defaultValue" which is wrong (actually correct in terms of how React works with useState however wrong in terms of the end business goal). The correct would be test123
You start digging into what might be the reason and come to realization that you actually don't need to put isBrowser ?
there at all because window.location.href
in the browser is actually defined because after all literally the log you see is being printed in browser's console so why useIsBrowser
is false, you start to wonder. When you remove isBrowser ?
the code starts working but then the build fails with the above message again. Then you either go back to the documentation to read about that non-bold part easy to miss or dig into the source code of Docusaurus like I did to learn about Perils of Hydration.
As it is very cumbersome that you have to not only compensate now for the value being undefined, you also need to compensate for the fact that it may have falled back to another value. You start adding isBrowser checks for those parts too like const someQueryParam = isBrowser ?
useState(isBrowser ?
then it starts to not feel the right thing to do and look for another solution. I ended up doing this Stackblitz example with a proxy. Include ?params=test123
in the URL and the value rendered is now the expected value. Just to remind you that I did not read about ExecutionEnvironment yet at this point but even if I did, I would still be forced to do this proxy implementation with ExecutionEnvironment.canUseDom instead of traditionalIsBrowser
in that code.
I would normally suggest changing the implementation of useIsBrowser
to typeof window !== 'undefined'
because as the developer, the goal of yours is to have your app build successfully by bypassing window being undefined. You don't care about perils of hydration, you just want to get your app built. But I've run into the problem of mismatching rendered HTML tons of times in NextJS and I know how painful it is to debug that so it makes sense to keep the best practice and don't change the implementation.
So an alternative we can do is to change the name of useIsBrowser
to useHasHydrationCompleted
but that would lead to many dependent applications breaking on the next version. Then we could add another hook useIsWindowDefined
that does typeof window !== 'undefined'
and then document it together with hasHydrationCompleted
so people that are just getting started could use them together. Existing applications could maybe utilize codemods, and on next upgrade of an existing application, the codemod could run automatically and rename it and if it is possible that the codemod can add, then also add the useIsWindowDefined
automatically. We can also link in the documentation simplified and shorter version of The perils of Rehydration article to help them explain why both of them need to be used.
Alternatively, if we don't change any names and leave it misleading as is, I have another suggestion in addition to having new hook called useIsWindowDefined
(as changing name of ExecutionEnvironment, is yet another set of breaking changes and codemodes to write):
- improve the error message printed when build fails due to
window
problems to also documentuseIsBrowser
linking to the documentation
It looks like you are using code that should run on the client-side only.
To get around it, try using one of:
<BrowserOnly>
(https://docusaurus.io/docs/docusaurus-core/#browseronly)useIsBrowser
anduseIsWindowDefined
hooks together (https://docusaurus.io/docs/advanced/ssg#useisbrowser)ExecutionEnvironment
(https://docusaurus.io/docs/docusaurus-core/#executionenvironment)It might also require to wrap your client code in
useEffect
hook and/or import a third-party library dynamically (if any).
- improve useIsBrowser section that explains after first client render part a bit more clearly:
You can also use the useIsBrowser() hook to test if the component has completed it's hydration. It returns false in SSR and true is CSR but only after first client render. If your build fails because window is undefined, and you actually need to check if you are in browser environment, you should combine this hook with
useWindowIsDefined
hook because this hook will returnfalse
when actually in browser environment and hydration has not completed and this may result in some of your code client side code that also rely onwindow
not working properly. The example below kind of indicates that window.location.href is only available after first client render has completed, however it is actually already available when typeof window is defined, even before the first hydration has completed.Use this hook if you only need to perform certain conditional operations on client-side, but not render an entirely different UI.
I can have a PR for this, but I am not a native English speaker thus the wording would require a review on the PR and also I just need confirmation which path to take, as what I think "may be right" could be wrong for someone else thus we should come up with an optimal solution as the community then create a PR for it
Self-service
- I'd be willing to address this documentation request myself.