How to Create a Custom React Hook for LocalStorage Using React Context

Sazzadur RahmanSazzadur Rahman
6 min read

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 then typescript 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 the store and setStore from StoreContext.

  • 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 the localValue with local storage and the context whenever localValue, key, or storeInLocalStorage 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 the localValue 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.tsFile:

// 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!

1
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.