DEV Community

Daniel Bellmas
Daniel Bellmas

Posted on

useStorage-Custom Hook in Next.js

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

A brief rundown

  • We have 2 functions getStorage and setStorage that are responsible for getting and parsing and setting 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 });
Enter fullscreen mode Exit fullscreen mode

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.

Thank you for reading!

Top comments (3)

nicatspark profile image

Is it really ok to have the internal hooks (useState, useEffect) render conditionally?

akmjenkins profile image
Adam • Edited

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.

danielbellmas profile image
Daniel Bellmas

Yeah, I thought it wouldn’t work too but this is a real life example that I use in Next.js :)