Hone logo
Hone
Problems

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:

  1. Store and Retrieve Data: Accept a key and an initial defaultValue. It should attempt to read the value from localStorage using the provided key. If no value exists in localStorage for that key, it should use the defaultValue.
  2. Provide State: Return a tuple containing the current value and a function to update that value. This behavior should mimic React's built-in useState hook.
  3. Persist Changes: Whenever the update function is called with a new value, the hook should automatically save this new value to localStorage under the specified key.
  4. Handle Serialization/Deserialization: localStorage can only store strings. Therefore, the hook must handle the serialization (e.g., using JSON.stringify) when storing complex data types (like objects or arrays) and deserialization (e.g., using JSON.parse) when reading them.
  5. Initial Value Handling: The defaultValue should be used only if the key is not found in localStorage on initial load.
  6. Event Handling (Optional but Recommended): Consider how to handle scenarios where localStorage is updated by another tab or window. A robust solution might involve listening to storage events.

Key Requirements

  • The hook must be written in TypeScript.
  • It should accept a generic type T for the data being stored.
  • It must return a tuple [value: T, setValue: (newValue: T) => void].
  • Data written to localStorage should be JSON stringified.
  • Data read from localStorage should be JSON parsed.
  • Handle cases where localStorage might not be available (e.g., server-side rendering, private browsing modes where localStorage is 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 value will be either the previously stored value from localStorage or the defaultValue.
  • Calling the setValue function with a new value will update the component's state and persist the new value to localStorage.
  • Subsequent renders will retrieve the latest value from localStorage.

Edge Cases to Consider

  • localStorage Not Available: The hook should gracefully handle environments where localStorage is not accessible (e.g., return the defaultValue and a functional setValue that doesn't interact with localStorage).
  • Invalid Data in localStorage: If the data stored under the key in localStorage is not valid JSON, the hook should default to the provided defaultValue.
  • 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).
  • localStorage API: Strictly use window.localStorage. Do not use sessionStorage or other browser storage mechanisms.
  • Error Handling: Implement robust error handling for JSON.parse and localStorage operations.
  • Performance: The hook should be efficient and avoid unnecessary re-renders.

Notes

  • Consider how to handle localStorage not being available (e.g., during SSR or in private browsing modes). A common pattern is to check for window.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 localStorage from other tabs/windows. This would involve listening to the storage event.
Loading editor...
typescript