Implement useLocalStorage Custom Hook in React
This challenge focuses on building a reusable custom React hook, useLocalStorage, that allows components to seamlessly interact with the browser's localStorage API. This hook will provide a stateful way to manage data stored in localStorage, automatically handling reads, writes, and updates across different components.
Problem Description
You are tasked with creating a custom React hook named useLocalStorage that abstracts away the complexities of using the browser's localStorage API. This hook should:
- Store and Retrieve Data: Accept a
keyand an initialdefaultValue. It should attempt to read the value fromlocalStorageusing the providedkey. If no value exists inlocalStoragefor thatkey, it should use thedefaultValue. - Provide State: Return a tuple containing the current value and a function to update that value. This behavior should mimic React's built-in
useStatehook. - Persist Changes: Whenever the update function is called with a new value, the hook should automatically save this new value to
localStorageunder the specifiedkey. - Handle Serialization/Deserialization:
localStoragecan only store strings. Therefore, the hook must handle the serialization (e.g., usingJSON.stringify) when storing complex data types (like objects or arrays) and deserialization (e.g., usingJSON.parse) when reading them. - Initial Value Handling: The
defaultValueshould be used only if thekeyis not found inlocalStorageon initial load. - Event Handling (Optional but Recommended): Consider how to handle scenarios where
localStorageis updated by another tab or window. A robust solution might involve listening tostorageevents.
Key Requirements
- The hook must be written in TypeScript.
- It should accept a generic type
Tfor the data being stored. - It must return a tuple
[value: T, setValue: (newValue: T) => void]. - Data written to
localStorageshould be JSON stringified. - Data read from
localStorageshould be JSON parsed. - Handle cases where
localStoragemight not be available (e.g., server-side rendering, private browsing modes wherelocalStorageis disabled). - Handle potential errors during JSON parsing (e.g., corrupted data in
localStorage).
Expected Behavior
When useLocalStorage is used in a component:
- On initial render, the
valuewill be either the previously stored value fromlocalStorageor thedefaultValue. - Calling the
setValuefunction with a new value will update the component's state and persist the new value tolocalStorage. - Subsequent renders will retrieve the latest value from
localStorage.
Edge Cases to Consider
localStorageNot Available: The hook should gracefully handle environments wherelocalStorageis not accessible (e.g., return thedefaultValueand a functionalsetValuethat doesn't interact withlocalStorage).- Invalid Data in
localStorage: If the data stored under thekeyinlocalStorageis not valid JSON, the hook should default to the provideddefaultValue. - Concurrent Updates: While not strictly required for a basic implementation, a more advanced solution might consider how to handle updates initiated from other browser tabs.
Examples
Example 1: Storing a simple string
import { useLocalStorage } from './useLocalStorage'; // Assuming your hook is in this file
function MyComponent() {
const [theme, setTheme] = useLocalStorage<string>('theme', 'light');
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme('dark')}>Set Dark Theme</button>
<button onClick={() => setTheme('light')}>Set Light Theme</button>
</div>
);
}
Input: User clicks "Set Dark Theme".
Output:
The component re-renders, displaying "Current theme: dark". The browser's localStorage will have an entry 'theme': '"dark"'.
Explanation:
The setTheme function updates the local state and writes the string 'dark' (after JSON stringification) to localStorage under the key 'theme'.
Example 2: Storing an object
import { useLocalStorage } from './useLocalStorage';
interface UserProfile {
name: string;
age: number;
}
function UserProfileComponent() {
const initialProfile: UserProfile = { name: 'Guest', age: 0 };
const [profile, setProfile] = useLocalStorage<UserProfile>('userProfile', initialProfile);
return (
<div>
<p>Name: {profile.name}, Age: {profile.age}</p>
<button onClick={() => setProfile({ name: 'Alice', age: 30 })}>Set Alice</button>
<button onClick={() => setProfile({ name: 'Bob', age: 25 })}>Set Bob</button>
</div>
);
}
Input: The component mounts. Then, the user clicks "Set Alice".
Output:
On mount, the component displays "Name: Guest, Age: 0". After clicking "Set Alice", it displays "Name: Alice, Age: 30". localStorage will contain 'userProfile': '{"name":"Alice","age":30}'.
Explanation:
The initialProfile is used if userProfile is not found in localStorage. When setProfile is called, the object { name: 'Alice', age: 30 } is JSON stringified and saved.
Example 3: Handling initial defaultValue when localStorage is empty
Input:
A user visits the site for the first time, and localStorage is empty or does not contain the key 'counter'.
import { useLocalStorage } from './useLocalStorage';
function CounterComponent() {
const [count, setCount] = useLocalStorage<number>('counter', 0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Output: The component displays "Count: 0".
Explanation:
Since 'counter' is not found in localStorage, the defaultValue of 0 is used. The setCount function will then correctly update both the component state and localStorage on subsequent calls.
Constraints
- TypeScript Version: Use TypeScript 4.0 or later.
- React Version: Assume React 16.8 or later (for hooks).
localStorageAPI: Strictly usewindow.localStorage. Do not usesessionStorageor other browser storage mechanisms.- Error Handling: Implement robust error handling for
JSON.parseandlocalStorageoperations. - Performance: The hook should be efficient and avoid unnecessary re-renders.
Notes
- Consider how to handle
localStoragenot being available (e.g., during SSR or in private browsing modes). A common pattern is to check forwindow.localStorage's existence. - When dealing with complex types, ensure proper JSON serialization and deserialization.
- Think about the return type of your hook. It should be a tuple, similar to
useState. - For an extra challenge, consider how to make the hook reactive to changes in
localStoragefrom other tabs/windows. This would involve listening to thestorageevent.