Hone logo
Hone
Problems

Create a useLocalStorageState React Hook

In React applications, it's often necessary to persist certain pieces of state across browser sessions or page reloads. This hook will allow you to easily manage state that is synchronized with the browser's localStorage.

Problem Description

Your task is to create a custom React hook called useLocalStorageState. This hook should accept a key (string) for localStorage and an initialState (any serializable value). It should return an array containing the current state value and a function to update that state, similar to useState.

The key requirements are:

  1. Initialization: When the hook is first used, it should attempt to retrieve the value associated with the provided key from localStorage. If a value exists, it should be used as the initial state. Otherwise, the provided initialState should be used.
  2. State Management: The hook should behave like useState, returning the current state and a setter function.
  3. Persistence: Whenever the state is updated via the setter function, the new state value should be automatically saved to localStorage under the given key.
  4. Serialization/Deserialization: Values stored in localStorage must be strings. You will need to use JSON.stringify to serialize values before saving and JSON.parse to deserialize them when retrieving.
  5. Type Safety: The hook should be written in TypeScript and leverage generics to ensure type safety for the state value.

Edge Cases to Consider:

  • Empty localStorage: What happens if the localStorage for the given key is empty or null?
  • Invalid JSON: What happens if the value stored in localStorage is not valid JSON? (Your implementation should handle this gracefully by defaulting to the initialState in such cases).
  • Server-Side Rendering (SSR): localStorage is a browser API and is not available on the server. Your hook should be robust enough to work correctly during SSR by checking for the existence of window.

Examples

Example 1: Basic Usage

import { renderHook, act } from '@testing-library/react';
import useLocalStorageState from './useLocalStorageState';

// Assume localStorage is cleared before this test
localStorage.clear();

const { result } = renderHook(() => useLocalStorageState('username', 'Guest'));

// Initial state should be the provided initialState if localStorage is empty
expect(result.current[0]).toBe('Guest');

// Update the state
act(() => {
  result.current[1]('Alice');
});

// State should be updated
expect(result.current[0]).toBe('Alice');

// Value should be persisted in localStorage
expect(localStorage.getItem('username')).toBe(JSON.stringify('Alice'));

Example 2: Retrieving from localStorage

// Assume 'userSettings' was previously set in localStorage
localStorage.setItem('userSettings', JSON.stringify({ theme: 'dark', notifications: true }));

const { result } = renderHook(() => useLocalStorageState<{ theme: string; notifications: boolean }>('userSettings', { theme: 'light', notifications: false }));

// Initial state should be retrieved from localStorage
expect(result.current[0]).toEqual({ theme: 'dark', notifications: true });
expect(result.current[0].theme).toBe('dark');

// Update the state
act(() => {
  result.current[1]({ theme: 'light', notifications: false });
});

// State should be updated
expect(result.current[0]).toEqual({ theme: 'light', notifications: false });
expect(localStorage.getItem('userSettings')).toBe(JSON.stringify({ theme: 'light', notifications: false }));

Example 3: Handling Invalid JSON

// Store invalid JSON in localStorage
localStorage.setItem('invalidData', 'this is not JSON');

const { result } = renderHook(() => useLocalStorageState('invalidData', 'default'));

// The hook should gracefully fall back to the initialState
expect(result.current[0]).toBe('default');
expect(localStorage.getItem('invalidData')).toBe('this is not JSON'); // The invalid data should remain untouched

Constraints

  • The hook must be written in TypeScript and utilize generics.
  • The key parameter must be a non-empty string.
  • The initialState can be any value that can be serialized by JSON.stringify (primitives, arrays, objects, null, etc.).
  • The hook must correctly handle cases where window is not defined (e.g., during SSR).
  • The hook should be efficient and avoid unnecessary re-renders.

Notes

  • Remember to handle potential errors during JSON.parse. A try...catch block is recommended.
  • Consider how to handle null or undefined values when storing and retrieving from localStorage. JSON.stringify(null) results in "null", which is valid.
  • The initialState might be a function that returns a value. Your hook should support this by calling the function if it's provided as the initialState. This is a common pattern in useState for performance when the initial state is expensive to compute.
Loading editor...
typescript