If you're using Next.js you know that it doesn't get along with localStorage
(or any storage for that matter).
That's because the storages are located under the global object windows
, which is equal to undefined
on the server, so we have to explicitly tell Next.js what to run in the server and what on the client.
First, I added a util that indicates if we are in SSR (server side rendering):
export const isSsr = typeof window === 'undefined';
The hook 🪝
import { useState, useEffect } from 'react';
import { isSsr } from '@/utils/isSsr';
export const getStorage = (storage, key) => JSON.parse(storage.getItem(key));
export const setStorage = (storage, key, newValue) => storage.setItem(key, JSON.stringify(newValue));
const useStorage = (storageType, key, initialValue) => {
if (isSsr) return [initialValue];
const storageName = `${storageType}Storage`;
const storage = window[storageName];
const [value, setValue] = useState(getStorage(storage, key) || initialValue);
useEffect(() => {
setStorage(storage, key, value);
}, [value]);
return [value, setValue];
};
export default useStorage;
A brief rundown
We have 2 functions
getStorage
andsetStorage
that are responsible forgetting and parsing
andsetting and stringifying
the data respectively.Before writing the logic that uses the
window
object I told Next.js to return the initial value.Every time the value changes the hook will update the chosen storage.
How to use
const LOCAL_STORAGE_KEY = 'filters';
const initialStateFilters = { isExpended: true };
const [filters, setFilters] = useStorage('local', LOCAL_STORAGE_KEY, initialStateFilters);
// The value
const { isExpended } = filters;
// Setting the value
const handleToggle = newIsExpended => setFilters({ ...filters, isExpended: newIsExpended });
You're maybe wondering why I'm using an object as the value of the data, it is simply for scalability reasons.
In the future we'll probably want to add more filters, so instead of creating a hook for each one we'll have them all under the same key.
Top comments (3)
Is it really ok to have the internal hooks (useState, useEffect) render conditionally?
In this case it is, but it's because of the implementation. React hooks only work if they are called in the same order every time a mounted component is rendered. If a mounted component is re-rendered with a different number of hook calls than when it was mounted (or last rendered), then React is unable to determine which hook calls refers to which original hook call - React relies on the order the hooks are called in to know how to persist state/cleanup. I was thinking about this and I thought that if hooks had distinct userland names this would solve the problem, but I'm guessing this wasn't deemed that useful/important by the React team.
In this example, the component only ever gets rendered once on the server, never re-rendered. And, when on the client, it'll never get re-rendered with a different number of hooks.
While your linter will complain, react won't have a problem handling this internally. But, 99.999% of the time, you should just obey the rules of hooks, even when you absolutely 100% know exactly what you're doing, because the person who's later tasked with maintaining your code might not.
Yeah, I thought it wouldn’t work too but this is a real life example that I use in Next.js :)