How to Write a Custom use-storage Hook in React
Client side react with use storage
The summary of the changes:
- We now use the
Storage
interface to handle bothlocalStorage
andsessionStorage
. 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
andremoveItem
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 theremoveItem
function. - We provide two new hooks:
useSessionStorage
anduseLocalStorage
, which simply call theuseStorage
function with the appropriateStorage
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.