The source code for this blog is available on GitHub.
Note
Top

How to Write a Custom use-storage Hook in React

Cover Image for How to Write a Custom use-storage Hook in React
Chen Han
Chen Han

Client side react with use storage

The summary of the changes:

  • We now use the Storage interface to handle both localStorage and sessionStorage. This makes the code more flexible and easier to test.
  • We use the useState hook with a function that initializes the state to the value of the storage item. This ensures that the state is only initialized once, when the component mounts.
  • We handle errors when reading and writing to storage, and log them to the console.
  • We added a setItem and removeItem function to allow setting and removing storage items from the component using the returned array.
  • We changed the function signature to return an array with three elements: the value of the storage item, the setItem function, and the removeItem function.
  • We provide two new hooks: useSessionStorage and useLocalStorage, which simply call the useStorage function with the appropriate Storage object.
import { useEffect, useState } from "react";

const useStorage = (key: string, storageType: Storage) => {
  const [state, setState] = useState(() => {
    try {
      const item = storageType.getItem(key);
      return item ? JSON.parse(item) : null;
    } catch (error) {
      console.error(error);
      return null;
    }
  });

  useEffect(() => {
    const handleChange = (event: StorageEvent) => {
      if (event.key === key) {
        try {
          const newValue = JSON.parse(event.newValue);
          setState(newValue);
        } catch (error) {
          console.error(error);
        }
      }
    };

    window.addEventListener("storage", handleChange);

    return () => {
      window.removeEventListener("storage", handleChange);
    };
  }, [key]);

  const setItem = (value: any) => {
    try {
      const newValue = JSON.stringify(value);
      storageType.setItem(key, newValue);
      setState(value);
    } catch (error) {
      console.error(error);
    }
  };

  const removeItem = () => {
    try {
      storageType.removeItem(key);
      setState(null);
    } catch (error) {
      console.error(error);
    }
  };

  return [state, setItem, removeItem];
};

export const useSessionStorage = (key: string) => useStorage(key, window.sessionStorage);
export const useLocalStorage = (key: string) => useStorage(key, window.localStorage);

To use the useSessionStorage hook with state, setItem, and removeItem, you can simply destructure the returned array like this:

import { useSessionStorage } from './useStorage';

function MyComponent() {
  const [value, setValue, removeValue] = useSessionStorage('myKey');

  const handleClick = () => {
    setValue('new value');
  };

  const handleRemove = () => {
    removeValue();
  };

  return (
    <div>
      <p>Value: {value}</p>
      <button onClick={handleClick}>Set Value</button>
      <button onClick={handleRemove}>Remove Value</button>
    </div>
  );
}

Server side Next.js with use storage

The window object is not available on the server-side in Next.js, as it is a browser-specific object. Therefore, you can't use window.sessionStorage or window.localStorage directly in your Next.js code.

To solve this issue, you can check if the window object is available before using it. You can do this by using the typeof operator to check if the window object is defined:

const isBrowser = typeof window !== 'undefined';

if (isBrowser) {
  // Use window.sessionStorage or window.localStorage here
}

In the useSessionStorage hook, you can wrap the code that uses sessionStorage inside this check:

import { useEffect, useState } from "react";
import { parseJson } from "@/util/generic";

const useStorage = (key: string, storageType: "local" | "session" = "local") => {
  const [state, setState] = useState(() => {
    try {
      const item = storageType === "session" ? sessionStorage.getItem(key) : localStorage.getItem(key);
      return item ? parseJson(item) : null;
    } catch (error) {
      console.error(error);
      return null;
    }
  });

  useEffect(() => {
    const handleChange = (event: StorageEvent) => {
      if (event.key === key) {
        try {
          const newValue = parseJson(event.newValue);
          setState(newValue);
        } catch (error) {
          console.error(error);
        }
      }
    };

    if (typeof window !== "undefined") {
      window.addEventListener("storage", handleChange);
    }

    return () => {
      if (typeof window !== "undefined") {
        window.removeEventListener("storage", handleChange);
      }
    };
  }, [key]);

  const setItem = (value: any) => {
    try {
      const newValue = JSON.stringify(value);
      const storage = storageType === "session" ? sessionStorage : localStorage;
      storage.setItem(key, newValue);
      setState(value);
    } catch (error) {
      console.error(error);
    }
  };

  const removeItem = () => {
    try {
      const storage = storageType === "session" ? sessionStorage : localStorage;
      storage.removeItem(key);
      setState(null);
    } catch (error) {
      console.error(error);
    }
  };

  return [state, setItem, removeItem];
};

In this modified version of the useStorage hook, you can pass in a second argument storageType that can be either "local" or "session", indicating which type of storage to use. The hook checks this value and uses either localStorage or sessionStorage accordingly.

The hook also checks if the window object is available before using it, by wrapping the code that accesses window inside an if statement. This ensures that the hook can be used on both the client and the server without errors.

Explain the useState section

const [state, setState] = useState(() => {
  try {
    const item = storageType === "session" ? 
          sessionStorage.getItem(key) : 
          localStorage.getItem(key);
    
    return item ? parseJson(item) : null;
  } catch (error) {
    console.error(error);
    return null;
  }
});

This code initializes the state of the hook by trying to retrieve an item from either session storage or local storage based on the storageType parameter. It first checks if the storageType is "session" and if so, it tries to get the item with the specified key from sessionStorage. If the storageType is "local", it tries to get the item from localStorage.

If the item exists, it is parsed using the parseJson function (presumably defined elsewhere) and returned as the initial state. If there is an error during this process, the error is logged to the console and null is returned as the initial state.

The useState hook is used to set the initial state to the retrieved item, which will be used throughout the component. The setState function returned by useState is then used to update this state throughout the component.

If I initialize the useState hook in Next.js with the following code, what will happen?

const [state, setState] = useState(() => {
  const item = storageType === "session" ? 
    sessionStorage.getItem(key) : 
    localStorage.getItem(key);
    
    return item ? parseJson(item) : null;
});

Thought process:

If I initialize the useState hook in Next.js with the code provided, it will attempt to retrieve the stored item from the storage type (session/local) based on the value of the storageType parameter passed in. If the item exists, it will be parsed using the parseJson function and returned as the initial state. If the item doesn't exist or there is an error parsing it, null will be returned as the initial state

Answer:

You can initialize the useState hook with a function in Next.js just like in any other React application. The code you provided is correct and should work as expected as long as storageType and key are defined.

The function you passed to useState is a "lazy initializer". It means that it will only be called during the initial render and not during re-renders. This is useful for expensive computations or accessing external data, like getting data from local or session storage.

In your case, the function initializes the state with the value of the item stored in local or session storage with the provided key. The parseJson function is called on the item value to convert it to an object if it's not null. The null value is returned if the item doesn't exist in storage or if there is an error during parsing.

What is the addEventListener and removeEventListener in useEffect?

useEffect(() => {
  const handleChange = (event: StorageEvent) => {
    if (event.key === key) {
      try {
        const newValue = parseJson(event.newValue);
        setState(newValue);
      } catch (error) {
        console.error(error);
      }
    }
  };

  if (typeof window !== "undefined") {
    window.addEventListener("storage", handleChange);
  }

  return () => {
    if (typeof window !== "undefined") {
      window.removeEventListener("storage", handleChange);
    }
  };
}, [key]);

In the given code, addEventListener is used to register a listener for the "storage" event that is fired when a change is made to the storage object (either localStorage or sessionStorage) in the same origin. The handleChange function is called whenever the "storage" event is fired and it checks if the event key matches the key passed as a dependency to the useEffect hook. If there is a match, it updates the state with the new value retrieved from the storage object.

removeEventListener is used to remove the registered listener when the component using the useStorage hook unmounts or when the key passed to useEffect changes. This is done to prevent memory leaks and unnecessary event listener calls.

Without the addEventListener and removeEventListener functions, the handleChange function would not be registered as a listener for the "storage" event, meaning that changes to the storage would not trigger the function to run.

Additionally, without removeEventListener, the listener function would continue to be active even after the component unmounts, which could lead to memory leaks or unexpected behavior.

Therefore, it's important to add the listener with addEventListener during the useEffect hook and then remove it with removeEventListener when the component is unmounted.

Why return function at the end of useEffect?

The return function at the end of useEffect is called the "cleanup function". It is used to clean up any resources that were created by the effect.

In this specific case, the cleanup function is removing the event listener that was added to the window object during the effect. This is important because if the event listener is not removed, it will continue to listen for storage events even after the component has been unmounted, which can lead to memory leaks and other issues.

By returning the cleanup function, useEffect will automatically invoke it when the component is unmounted or when the dependencies of the effect change, allowing us to clean up any resources that were created by the effect.

© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy