Shimmer Image
javascript

Building a Custom and Reusable React Hook for Local Storage: Step-by-Step Tutorial

Building a Custom and Reusable React Hook for Local Storage: Step-by-Step Tutorial
0 views
10 min read
#javascript

How To Create A Customizable And Reusable React Hook For Using Local Storage: A Step-by-Step Tutorial

In modern web development, managing client-side storage effectively is crucial. Whether you're storing user preferences, session data, or other persistent states, localStorage provides a simple way to store key-value pairs in the browser. However, using localStorage directly in a React application can be cumbersome and repetitive. This is where a custom React hook can make a significant difference.

In this article, we'll walk through how to create a highly customizable and reusable React hook for managing localStorage. This hook will allow you to store and retrieve data, handle serialization and deserialization, and provide options for customization such as default values and custom storage keys.

1. Understanding the Basics of localStorage

Before diving into the implementation, it's important to understand how localStorage works. localStorage is part of the Web Storage API, which allows you to store data as key-value pairs directly in the browser. The data stored in localStorage persists even after the browser is closed, making it ideal for saving user preferences and session data.

Some key methods you'll use with localStorage:

  • localStorage.setItem(key, value): Stores a key-value pair.
  • localStorage.getItem(key): Retrieves the value associated with a key.
  • localStorage.removeItem(key): Removes the key-value pair.
  • localStorage.clear(): Clears all key-value pairs.

2. Setting Up the Basic React Hook

Let's start by creating a basic custom React hook that interacts with localStorage. This hook will manage a piece of state, synchronize it with localStorage, and update localStorage whenever the state changes.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
    // Get the stored value or fallback to initialValue
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    // Sync state with localStorage whenever the stored value changes
    useEffect(() => {
        try {
            localStorage.setItem(key, JSON.stringify(storedValue));
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue]);

    return [storedValue, setStoredValue];
}

export default useLocalStorage;

advertisement

3. Enhancing the Hook with Customization Options

While the basic hook works well for simple use cases, it lacks flexibility and customization. Let's enhance the hook by adding features like:

  • Custom Serialization/Deserialization: Allow users to define how data is serialized and deserialized.
  • Callback Function on Value Change: Trigger a callback when the stored value changes.
  • Default Value Handling: Provide more robust handling of default values.
  • Expiration Time: Optionally set an expiration time for the stored data.

Custom Serialization/Deserialization

What Does "Custom Serialization and Deserialization" Mean?

Serialization is the process of converting data (like an object or array) into a string format that can be stored in localStorage. Deserialization is the reverse process—converting that stored string back into its original data format.

Why Would You Want Custom Serialization and Deserialization?

By default, data in localStorage is usually stored as a JSON string using JSON.stringify for serialization and retrieved using JSON.parse for deserialization. However, sometimes you might need more control over how your data is stored and retrieved. For example:

  • Custom Formats: You might want to store data as a Base64 string or encrypt it for security.
  • Complex Data: If you're dealing with more complex data structures, the default methods might not be enough.

How Does This Work in the Hook?

To make the useLocalStorage hook more flexible, we allow you to provide your own functions for serialization and deserialization. This way, you can control exactly how your data is transformed before it's stored and after it's retrieved.

function useLocalStorage(key, initialValue, options = {}) {
    const {
        serialize = JSON.stringify,
        deserialize = JSON.parse,
    } = options;

    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? deserialize(item) : initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    useEffect(() => {
        try {
            localStorage.setItem(key, serialize(storedValue));
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue, serialize]);

    return [storedValue, setStoredValue];
}

Callback Function on Value Change

Sometimes you may want to perform an action when the stored value changes, such as updating other parts of the app or logging an event. We can add an optional callback function that is called whenever the stored value changes.

function useLocalStorage(key, initialValue, options = {}) {
    const {
        serialize = JSON.stringify,
        deserialize = JSON.parse,
        onChange = null,
    } = options;

    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? deserialize(item) : initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    useEffect(() => {
        try {
            localStorage.setItem(key, serialize(storedValue));
            if (onChange) {
                onChange(storedValue);
            }
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue, serialize, onChange]);

    return [storedValue, setStoredValue];
}

advertisement

Handling Default Values

Sometimes, you may want to ensure that a default value is stored in localStorage if the key doesn't exist. We can handle this by checking if the key exists on initialization and storing the default value if it doesn't.

function useLocalStorage(key, initialValue, options = {}) {
    const {
        serialize = JSON.stringify,
        deserialize = JSON.parse,
        onChange = null,
        storeDefault = true,
    } = options;

    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            if (item === null && storeDefault) {
                localStorage.setItem(key, serialize(initialValue));
            }
            return item ? deserialize(item) : initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    useEffect(() => {
        try {
            localStorage.setItem(key, serialize(storedValue));
            if (onChange) {
                onChange(storedValue);
            }
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue, serialize, onChange]);

    return [storedValue, setStoredValue];
}

Expiration Time

To add even more utility, we can implement expiration times for the data stored in localStorage. This is particularly useful for session-based data.

function useLocalStorage(key, initialValue, options = {}) {
    const {
        serialize = JSON.stringify,
        deserialize = JSON.parse,
        onChange = null,
        storeDefault = true,
        expiration = null,
    } = options;

    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            if (item) {
                const parsedItem = deserialize(item);
                if (expiration && parsedItem.timestamp && (Date.now() - parsedItem.timestamp) > expiration) {
                    localStorage.removeItem(key);
                    return initialValue;
                }
                return parsedItem.value;
            } else if (storeDefault) {
                const valueToStore = {
                    value: initialValue,
                    timestamp: Date.now(),
                };
                localStorage.setItem(key, serialize(valueToStore));
            }
            return initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    useEffect(() => {
        try {
            const valueToStore = {
                value: storedValue,
                timestamp: Date.now(),
            };
            localStorage.setItem(key, serialize(valueToStore));
            if (onChange) {
                onChange(storedValue);
            }
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue, serialize, onChange]);

    return [storedValue, setStoredValue];
}

4. Putting It All Together

Here's the final version of the useLocalStorage hook, combining all the customization options we've discussed:

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue, options = {}) {
    const {
        serialize = JSON.stringify,
        deserialize = JSON.parse,
        onChange = null,
        storeDefault = true,
        expiration = null,
    } = options;

    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            if (item) {
                const parsedItem = deserialize(item);
                if (expiration && parsedItem.timestamp && (Date.now() - parsedItem.timestamp) > expiration) {
                    localStorage.removeItem(key);
                    return initialValue;
                }
                return parsedItem.value;
            } else if (storeDefault) {
                const valueToStore = {
                    value: initialValue,
                    timestamp: Date.now(),
                };
                localStorage.setItem(key, serialize(valueToStore));
            }
            return initialValue;
        } catch (error) {
            console.error("Error reading localStorage key", error);
            return initialValue;
        }
    });

    useEffect(() => {
        try {
            const valueToStore = {
                value: storedValue,
                timestamp: Date.now(),
            };
            localStorage.setItem(key, serialize(valueToStore));
            if (onChange) {
                onChange(storedValue);
            }
        } catch (error) {
            console.error("Error setting localStorage key", error);
        }
    }, [key, storedValue, serialize, onChange]);

    return [storedValue, setStoredValue];
}

export default useLocalStorage;

advertisement

5. Usage Examples

Here are some examples of how you can use the useLocalStorage hook in your React applications:

Basic Usage:

const [name, setName] = useLocalStorage('name', 'Guest');

With Custom Serialization/Deserialization:

const [user, setUser] = useLocalStorage('user', {}, {
    serialize: (value) => btoa(JSON.stringify(value)), // Base64 encode
    deserialize: (value) => JSON.parse(atob(value)),   // Base64 decode
});

With Expiration Time:

const [session, setSession] = useLocalStorage('session', {}, {
    expiration: 24 * 60 * 60 * 1000, // 24 hours
});

With Callback on Change:

const [theme, setTheme] = useLocalStorage('theme', 'light', {
    onChange: (newTheme) => console.log('Theme changed to:', newTheme),
});

Combining Options

You can combine options like custom serialization, expiration time, and a callback on change in a single usage:

const [user, setUser] = useLocalStorage('user', {}, {
    serialize: (value) => btoa(JSON.stringify(value)), // Custom serialization to Base64
    deserialize: (value) => JSON.parse(atob(value)),   // Custom deserialization from Base64
    expiration: 24 * 60 * 60 * 1000,                   // Expiration time: 24 hours
    onChange: (newValue) => console.log('User data updated:', newValue), // Callback on change
});

How It Works Together:

  • Custom Serialization/Deserialization: The data is serialized into a Base64-encoded string before being stored in localStorage and deserialized back to an object when retrieved.

  • Expiration Time: The stored data is checked to see if it has expired based on the timestamp stored alongside the value. If the data has expired, it is removed from localStorage, and the initial value is used instead.

  • Callback on Change: Whenever the storedValue changes, the onChange callback function is triggered, allowing you to react to changes (e.g., logging the update, triggering other actions).

Practical Example:

Imagine you have a user profile that you want to store in localStorage. You want to ensure that:

  1. The data is stored securely (e.g., as a Base64 string).
  2. The data expires after 24 hours.
  3. You are notified whenever the data changes.
const [userProfile, setUserProfile] = useLocalStorage('userProfile', {}, {
    serialize: (value) => btoa(JSON.stringify(value)), // Secure the data with Base64 encoding
    deserialize: (value) => JSON.parse(atob(value)),   // Decode the data from Base64 when retrieving
    expiration: 24 * 60 * 60 * 1000,                   // Set an expiration time of 24 hours
    onChange: (newProfile) => console.log('User profile updated:', newProfile), // Log changes
});

// Example of updating the user profile
setUserProfile({ name: 'John Doe', age: 30 });

More JavaScript Articles:

advertisement

If you enjoyed this article, please consider making a donation. Your support means a lot to me.

  • Cashapp: $hookerhillstudios
  • Paypal: Paypal

Conclusion

Creating a custom useLocalStorage React hook provides a reusable, customizable, and powerful way to manage local storage in your applications. By adding options like custom serialization, expiration times, and change callbacks, you can tailor this hook to meet the specific needs of your projects. This approach not only reduces repetitive code but also ensures a consistent and flexible way to manage persistent state across your application.

Feel free to expand upon this hook based on your requirements, and share it across your projects to make handling localStorage a breeze!

advertisement

Comments

to join the conversation

Loading comments...

About the Author

Jared Hooker

Hi, I'm Jared Hooker, and I have been passionate about coding since I was 13 years old. My journey began with creating mods for iconic games like Morrowind and Rise of Nations, where I discovered the thrill of bringing my ideas to life through programming.

Over the years, my love for coding evolved, and I pursued a career in software development. Today, I am the founder of Hooker Hill Studios, where I specialize in web and mobile development. My goal is to help businesses and individuals transform their ideas into innovative digital products.