This issue has to do with Server-Side Rendering in Next.js. Next.js will by default try to use SSR for your site. This means that since we’re on the server and not in the browser, the “window” object does not exist. The workaround for this is forcing Next.js to run your code in the browser, and I’ll explain how to do that.
Using the useEffect hook
The useEffect hook will always run in the browser, so we can use this to ensure that our code will only be run from there. For a quick primer on the hook, check out this article.
Here’s some example code for a simple app that stores the last time the user visited the site in local storage. If you’re unfamiliar with using localStorage in React or Next.js, check out this tutorial we have here, which uses a similar approach. The approach is identical, with the small difference that you can get away with using localStorage in the body of a vanilla React component since there is no SSR by default.
function updateLastSeen() {
const lastSeen = window.localStorage.getItem('last-seen') ?? new Date();
window.localStorage.setItem('last-seen', new Date().toString());
return lastSeen;
}
function WindowPage() {
const lastSeen = updateLastSeen();
return <div>Last Seen: {lastSeen}</div>;
}
Running this we get this error you’re probably familiar with:
To get around this, let’s introduce a custom hook that involves a useEffect.
function useLastSeen() {
const [lastSeen, setLastSeen] = useState(null);
const retrieved = useRef(false); //To get around strict mode running the hook twice
useEffect(() => {
if (retrieved.current) return;
retrieved.current = true;
setLastSeen(updateLastSeen());
}, []);
return lastSeen;
}
I’ve moved our logic to a custom hook, simply to keep the code clean. Now with our code changes, the code will only run when the hook runs, which is on the client.
We can then update our component to:
function WindowPage() {
const lastSeen = useLastSeen();
return (
<div>
Last Seen: {lastSeen}
</div>
);
}
The code is cleaner, and since the useEffect inside the hook only runs on the client, our error goes away!
Checking if Window is Defined
An alternative to this is to simply check if the window object is defined before we run our code. If the code is running on the server, since we aren’t in the browser, the window object won’t exist.
We can’t use the same example as before as there is one key difference to this approach. Since we aren’t waiting for the component to render, any differences to the HTML for the page will cause the server-side rendered version of the page to differ from the client-side, and we’ll get an error in Next.js.
In this example we’ll add an event listener to the window object, to track clicks on our page and their position.
const isBrowser = () => typeof window !== 'undefined'; //The approach recommended by Next.js
function WindowPage() {
const [lastClick, setLastClick] = useState('');
if (isBrowser()) { //Only add the event listener client-side
window.addEventListener('click', (e) =>
setLastClick(`${e.pageX}, ${e.pageY}`)
);
}
return (
<div className="m-auto rounded bg-violet-600 p-10 font-bold text-white shadow">
Click at: {lastClick}
</div>
);
}
As we can see, the code isn’t being run on the server, so Next.js is now happy!
Hopefully one of these two approaches has helped to solve your issue. I recommend the first approach, as it covers most cases, and you don’t have to deal with the possibility of Next.js finding a rendering mismatch. Feel free to leave a comment down below letting me know if I did or didn’t help with your problem, or if you simply liked the article!
💬 Leave a comment