How to Create a Custom React Hook for LocalStorage Using React Context
Managing local storage in React applications can become cumbersome, especially when you need to synchronize data across multiple components. By creating a custom hook combined with React Context, you can simplify this process and make your code more reusable and maintainable. In this article, I’ll guide you through creating a custom React hook for local storage and demonstrate how to integrate it using React Context.
Setting Up the Project
First, let’s set up a new React project using Vite with TypeScript
for better type safety.
Open your terminal and execute the following command to create a new Vite project:
pnpm create vite@latest react-store-hook
During the setup, select
react
and thentypescript
to ensure your project uses TypeScript.
Navigate to the project directory and install the dependencies:
cd react-store-hook
pnpm install
Setting Up the React Context
Next, we’ll set up a React Context to provide our custom hook across the application. This way, we can access the local storage state in any component.
First, define the type for the context:
Create a folder types
inside the src
folder and a file index.d.ts
inside the folder.
// src/types/index.d.ts
declare type StoreContextType = {
[key: string]: unknown;
};
Now create a folder named hooks
inside src
folder.
Create the context file:
Create a StoreContext.ts
file. The StoreContext
will provide the global state and a method to update it across your application.
// src/hooks/StoreContext.ts
import { createContext, Dispatch, SetStateAction } from "react";
export const StoreContext = createContext<{
store: StoreContextType;
setStore: Dispatch<SetStateAction<StoreContextType>>;
}>({
store: {},
setStore: () => {},
});
Next, create the provider component:
Create a StoreProvider.tsx
file. The StoreProvider
component will wrap your application and provide the StoreContext
to its children.
// src/hooks/StoreProvider.tsx
import { useState, ReactNode, FC } from "react";
import { StoreContext } from "./StoreContext";
const StoreProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [store, setStore] = useState<StoreContextType>({});
return <StoreContext.Provider value={{ store, setStore }}>{children}</StoreContext.Provider>;
};
export { StoreProvider };
Creating the Custom Hook: useStore
Now let’s create the custom hook useStore
. This hook will interact with both the StoreContext
and local storage. Create a useStore.ts
file.
Add the following code step by step touseStore.ts
:
Step 1: Import Necessary Modules
import { useContext, useEffect, useCallback, useState } from "react";
import { StoreContext } from "./StoreContext";
Here, we import React hooks and the StoreContext
. We'll use these to manage state and context.
Step 2: Define the Hook Signature
const useStore = <T>(
key: string,
initialValue?: T | null,
storeInLocalStorage: boolean = true
): [T, (value: T) => void, boolean] => {
The useStore
hook takes three parameters:
key
: The key to store the value under.initialValue
: The initial value to set.storeInLocalStorage
: A flag to determine if the value should be stored in local storage.
It returns a array containing:
The stored value.
A function to update the value.
A loading state.
Step 3: Initialize the State
const { store, setStore } = useContext(StoreContext);
const initializeState = useCallback(() => {
if (storeInLocalStorage && typeof window !== "undefined") {
const storedValue = localStorage.getItem(key);
return storedValue !== null ? JSON.parse(storedValue) : initialValue;
}
return initialValue || null;
}, [key, initialValue, storeInLocalStorage]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [localValue, setLocalValue] = useState<T | undefined>(initializeState);
We use
useContext
to get thestore
andsetStore
fromStoreContext
.initializeState
is a function that gets the initial state from local storage if the flag is set and the window object is available. Otherwise, it returns the initial value.isLoading
indicates if the state is being initialized.localValue
holds the current value.
Step 4: Sync State with Local Storage and Context
useEffect(() => {
setIsLoading(true);
if (storeInLocalStorage && localValue !== undefined) {
localStorage.setItem(key, JSON.stringify(localValue));
}
setStore(prevStore => ({
...prevStore,
[key]: localValue,
}));
setIsLoading(false);
}, [key, localValue, storeInLocalStorage, setStore]);
The
useEffect
hook syncs thelocalValue
with local storage and the context wheneverlocalValue
,key
, orstoreInLocalStorage
changes.It updates local storage if the flag is set and sets the value in the context.
Step 5: Create the Set Value Function
const setValue = useCallback(
(value: T) => {
setLocalValue(value);
setStore(prevStore => ({
...prevStore,
[key]: value,
}));
if (storeInLocalStorage) {
localStorage.setItem(key, JSON.stringify(value));
}
},
[key, storeInLocalStorage, setStore]
);
The
setValue
function updates thelocalValue
and the context.It also updates local storage if the flag is set.
Step 6: Return the Hook Values
return [store[key] !== undefined ? (store[key] as T) : (localValue as T), setValue, isLoading];
};
export default useStore;
The hook returns the current value, the setValue
function, and the loading state.
FulluseStore.ts
File:
// src/hooks/useStore.ts
import { useContext, useEffect, useCallback, useState } from "react";
import { StoreContext } from "./StoreContext";
const useStore = <T>(
key: string,
initialValue?: T | null,
storeInLocalStorage: boolean = true
): [T, (value: T) => void, boolean] => {
const { store, setStore } = useContext(StoreContext);
const initializeState = useCallback(() => {
if (storeInLocalStorage && typeof window !== "undefined") {
const storedValue = localStorage.getItem(key);
return storedValue !== null ? JSON.parse(storedValue) : initialValue;
}
return initialValue || null;
}, [key, initialValue, storeInLocalStorage]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [localValue, setLocalValue] = useState<T | undefined>(initializeState);
useEffect(() => {
setIsLoading(true);
if (storeInLocalStorage && localValue !== undefined) {
localStorage.setItem(key, JSON.stringify(localValue));
}
setStore(prevStore => ({
...prevStore,
[key]: localValue,
}));
setIsLoading(false);
}, [key, localValue, storeInLocalStorage, setStore]);
const setValue = useCallback(
(value: T) => {
setLocalValue(value);
setStore(prevStore => ({
...prevStore,
[key]: value,
}));
if (storeInLocalStorage) {
localStorage.setItem(key, JSON.stringify(value));
}
},
[key, storeInLocalStorage, setStore]
);
return [store[key] !== undefined ? (store[key] as T) : (localValue as T), setValue, isLoading];
};
export default useStore;
Putting It All Together
Finally, let’s see how to use the StoreProvider
and useStore
hook in your application.
In the main.tsx
file:
// src/main.tsx
import { StoreProvider } from "./hooks/StoreProvider";
// other imports
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<StoreProvider>
<App />
</StoreProvider>
</React.StrictMode>
);
In the component where you want to use the hook:
// src/MyComponent.tsx
import React from 'react';
import useStore from './hooks/useStore';
const MyComponent: React.FC = () => {
const [value, setValue, isLoading] = useStore<string>('myKey', 'defaultValue');
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Stored Value: {value}</h1>
<button onClick={() => setValue('newValue')}>Set New Value</button>
</div>
);
};
export default MyComponent;
Conclusion
In this article, we’ve walked through the process of creating a custom React hook for managing local storage, integrating it with React Context to provide a clean and efficient solution for state management across your application. By encapsulating local storage logic within a hook, we can keep our components simpler and more focused on their core responsibilities. Using TypeScript enhances type safety and makes the code more robust.
The useStore
hook, combined with StoreContext
and StoreProvider
, provides a flexible and reusable way to manage local storage, ensuring that data is synchronized across components and reducing the need for repetitive code. This approach not only improves maintainability but also makes your application more scalable and easier to understand.
Implementing custom hooks and contexts in your React applications can significantly improve your development workflow. We encourage you to try integrating these patterns into your projects and experience the benefits of a cleaner, more maintainable codebase.
By following this guide, you’ll have an excellent tool for managing state in your React development toolbox. Feel free to experiment with this to tailor the useStore hook to your specific needs.
Happy coding!
Subscribe to my newsletter
Read articles from Sazzadur Rahman directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Sazzadur Rahman
Sazzadur Rahman
👋 Hey there! I'm a passionate developer with a knack for creating robust and user-friendly applications. My expertise spans across various technologies, including TypeScript, JavaScript, SolidJS, React, NextJS.