Today, let’s talk about hydration
error that often occurs in Next.js
. Most of us used Next.js
must be familiar with the following error:
**Error**: Hydration failed because the initial UI does not match what was rendered on the server.
The reason for the error has been clearly stated in the error message: the **hydration**
failed due to the inconsistency between the UI after hydration and the UI rendered by the server. Although the error message says hydration failed, however, it still shows the correct UI that can be considered rendered successful.
Let’s talk about a business case
We stored some unimportant or unsynchronized data locally and get these locally stored data on the client side. For example, we have some configurations stored in localStorage
, and the page will be rendered according to the configuration.
Take sidebar
as an example:
export default function R() {
const [expand, setExpand] = React.useState(
() => localStorage.getItem(EXPAND_STORAGE_KEY) === "1"
);
return (
<div>
<NavSidebar expand={expand} onExpand={setExpand} />
</div>
);
}
We have a sidebar that the user can expand or collapse, we often save its status locally for user experience.
As Next.js has client render(CSR
) and server render(SSR
) both, the code will run correctly on the client side but will result in an error on the server rendering (SSR
) because the server side lackslocalStorage
API. So we modified the code:
export default function R() {
const [expand, setExpand] = React.useState(() =>
typeof window === "undefined"
? false
: localStorage.getItem(EXPAND_STORAGE_KEY) === "1"
);
return (
<div>
<NavSidebar expand={expand} onExpand={setExpand} />
</div>
);
}
We check if it is a server or a browser environment by window
keyword, and then run different codes according to it, then the error we mentioned at the beginning will disappear.
The above code looks good logically, but the abovehydration
error will come out when you run it in the browser.
React Do The Check Instead of Next.js
In fact, the error was caused by a check of react-dom
in React
instead of Next.js.
We can simply look at the relevant source code in react-dom
:
if (!tryHydrate(fiber, nextInstance)) {
if (shouldClientRenderOnMismatch(fiber)) {
warnNonhydratedInstance(hydrationParentFiber, fiber);
throwOnHydrationMismatch();
}
}
function throwOnHydrationMismatch(fiber) {
throw new Error(
"Hydration failed because the initial UI does not match what was " +
"rendered on the server."
);
}
function shouldClientRenderOnMismatch(fiber) {
return (
(fiber.mode & ConcurrentMode) !== NoMode &&
(fiber.flags & DidCapture) === NoFlags
);
}
The code in react-dom
will use tryHydrate
to try the hydrate operation, if it fails, the mode and flags will be checked and the hydration
error will be thrown.
How to solve this error? There is 3 Solution For you.
Solution 1
useEffect/componentDidMount
To solve the above problems, it is officially recommended to use useEffect
:
const [expand, setExpand] = React.useState(true);
// to avoid ssr error
useEffect(() => {
setExpand(localStorage.getItem(EXPAND_STORAGE_KEY) === "1");
}, []);
There is no error will be thrown because the effect will not be executed on the server. Of course, you can also use class components, and then get localStorage
in componentDidMount
.
However, there are some problems that appear when using this solution. For example, if there is an animation for the expansion and collapse of the sideBar
, the user will see an extra animation when entering the page, which will be strange. The solution is not to render the sidebar
by default. 😂 Therefore, the effect of SSR
is not as good as expected, it really depends on the actual business.
Solution 2
react-no-ssr
Another solution is to use some open-source libraries, such as react-no-ssr
. In fact, react-no-ssr
is also implemented using the solution 1
. You can look at the source code:
import React from "react";
const DefaultOnSSR = () => <span></span>;
class NoSSR extends React.Component {
constructor(...args) {
super(...args);
this.state = {
canRender: false,
};
}
componentDidMount() {
this.setState({ canRender: true });
}
render() {
const { children, onSSR = <DefaultOnSSR /> } = this.props;
const { canRender } = this.state;
return canRender ? children : onSSR;
}
}
export default NoSSR;
It can be seen that NoSSR
will set canRender
only in componentDidMount
, then it will be rendered in the right way.
Solution 3
Turn Off SSR
We can solve the problem by closing SSR
of the component as well. In fact, the above react-no-ssr
is one of them, but the Next.js
also provide a built-in solution: load components dynamically and turn off **SSR**
, take the above sidebar
as an example:
import dynamic from "next/dynamic";
const DynamicSidebarWithNoSSR = dynamic(() => import("../components/Sidebar"), {
ssr: false,
});
export default function R() {
return (
<div>
<DynamicSidebarWithNoSSR />
</div>
);
}
We only need to extract the component, then use dynamic
to load the component and pass in the SSR
parameter as false
to turn off the server render of the component.
Of course, we can do some work for convenience:
import dynamic from "next/dynamic";
import React from "react";
const NoSSR = (props) => <React.Fragment>{props.children}</React.Fragment>;
export default dynamic(() => Promise.resolve(NoSSR), {
ssr: false,
});
Then we only need to directly call the NoSSR
to wrap the child component:
import dynamic from "next/dynamic";
import Sidebar from "../components/Sidebar";
export default function R() {
return (
<div>
<NoSSR>
<Sidebar />
</NoSSR>
</div>
);
}
Conclusion
CSR
is limited to running in the browser and SSR
needs to be able to run in both browser and server which meets some problems and challenges that not happened in pure CSR
apps. In terms of user experience, SSR
will indeed bring a great improvement to our application.