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:
- Initialization: When the hook is first used, it should attempt to retrieve the value associated with the provided
keyfromlocalStorage. If a value exists, it should be used as the initial state. Otherwise, the providedinitialStateshould be used. - State Management: The hook should behave like
useState, returning the current state and a setter function. - Persistence: Whenever the state is updated via the setter function, the new state value should be automatically saved to
localStorageunder the givenkey. - Serialization/Deserialization: Values stored in
localStoragemust be strings. You will need to useJSON.stringifyto serialize values before saving andJSON.parseto deserialize them when retrieving. - 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 thelocalStoragefor the given key is empty ornull? - Invalid JSON: What happens if the value stored in
localStorageis not valid JSON? (Your implementation should handle this gracefully by defaulting to theinitialStatein such cases). - Server-Side Rendering (SSR):
localStorageis 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 ofwindow.
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
keyparameter must be a non-empty string. - The
initialStatecan be any value that can be serialized byJSON.stringify(primitives, arrays, objects, null, etc.). - The hook must correctly handle cases where
windowis 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. Atry...catchblock is recommended. - Consider how to handle
nullorundefinedvalues when storing and retrieving fromlocalStorage.JSON.stringify(null)results in"null", which is valid. - The
initialStatemight be a function that returns a value. Your hook should support this by calling the function if it's provided as theinitialState. This is a common pattern inuseStatefor performance when the initial state is expensive to compute.