Session and Local storage
Table of contents
- Overview
- Configuration
- The storage Utility
- React Hooks
- Choosing Between Local and Session Storage
- Security Considerations
- Testing with Vitest
- API Reference
1. Overview Table of Contents
The storage utility gives your React views a safe way to save data in the browser using localStorage and sessionStorage. It adds several features that the browser APIs do not provide on their own:
- Namespaced keys - Every key is prefixed so your app’s data does not collide with other apps or libraries on the same origin.
- Automatic JSON serialization - Store objects, arrays, numbers, and booleans directly.
- Optional expiry - Values can be given a time to live (TTL) in seconds, the same unit used by
defaultTtlin the PHPApiclass. - Graceful fallback - If the browser blocks storage (private browsing, disabled site data, or a full quota), the utility falls back to in-memory storage instead of throwing errors.
- React hooks -
useLocalStorageanduseSessionStoragework likeuseState, but the value is saved in the browser and kept in sync across components and tabs.
Common uses include:
- Remembering user preferences such as units, theme, or page size
- Saving catalog filters and sort order while a user browses
- Autosaving drafts of long forms
- Tracking recently viewed items
- Keeping a guest’s shopping cart before they log in
2. Configuration Table of Contents
By default all keys are prefixed with chappy:. You can change this by adding the following to your .env file:
VITE_STORAGE_PREFIX=my_app
Setting a unique prefix is recommended when you run more than one Chappy.php project on the same address during development (for example localhost:8000). Browsers share storage between all pages on the same scheme, host, and port, so two apps using the same prefix could read or clear each other’s data.
NOTE: Only variables starting with VITE_ are exposed to the browser. Never put secrets in a VITE_ variable. Because Vite reads this value at build time, restart npm run dev or rebuild your assets after changing it.
3. The storage Utility Table of Contents
A. Importing
The utility exports two stores, local and session, which share the same methods.
import { local, session } from '@chappy/utils/storage';
You can also use the default export:
import storage from '@chappy/utils/storage';
storage.local.get('weather.units');
storage.session.get('catalog.filters');
B. Reading and Writing Values
Use set() to save a value and get() to read it back.
local.set('weather.units', 'metric');
const units = local.get('weather.units', 'imperial');
The second argument to get() is a fallback that is returned when the key does not exist, has expired, or contains invalid data. If no fallback is provided, null is returned.
Any JSON-serializable value can be stored:
session.set('catalog.filters', { genre: 'rpg', platform: 'switch', sort: 'price' });
const filters = session.get('catalog.filters', {});
set() returns true when the value was saved and false when it could not be saved, such as when the storage quota is full. It never throws, so your component will not crash if a write fails.
if (!local.set('recent.games', ids)) {
// Value was not saved. Continue without it.
}
C. Expiring Values with TTL
Pass a ttl option in seconds to make a value expire. Expired values are treated as missing and get() returns the fallback.
// Expires in one week
local.set('recent.games', ids, { ttl: 60 * 60 * 24 * 7 });
// Expires in 15 minutes
session.set('catalog.filters', filters, { ttl: 900 });
When no ttl is provided the value never expires.
D. Removing and Clearing Values
Remove a single key:
local.remove('weather.units');
List all keys saved by your app:
const keys = local.keys(); // ['weather.units', 'recent.games']
Remove every key saved by your app:
local.clear();
session.clear();
clear() only removes keys that use your prefix. Data saved by other apps or libraries on the same origin is left alone.
E. Fallback Behavior
Some browsers block storage, for example in private browsing mode or when the user has disabled site data. When this happens the store switches to in-memory storage. Your code keeps working, but data is lost when the page reloads.
You can check which mode a store is using with the persistent property:
if (!local.persistent) {
console.info('Preferences will not be saved after this page reloads.');
}
4. React Hooks Table of Contents
The hooks are imported from the same module:
import { useLocalStorage, useSessionStorage } from '@chappy/utils/storage';
Each hook returns an array with three items:
- The current value
- A setter function
- A function that removes the key
A. useLocalStorage
Saves a value in localStorage. The value persists after the browser is closed and is shared across all tabs.
function UnitToggle() {
const [units, setUnits] = useLocalStorage('weather.units', 'imperial');
return (
<select value={units} onChange={e => setUnits(e.target.value)} className="form-select">
<option value="imperial">Fahrenheit</option>
<option value="metric">Celsius</option>
</select>
);
}
The setter also accepts an updater function, just like useState:
const [recent, setRecent] = useLocalStorage('recent.games', [], { ttl: 60 * 60 * 24 * 7 });
function addRecent(id) {
setRecent(prev => [id, ...prev.filter(x => x !== id)].slice(0, 10));
}
The third argument is an options object. When ttl is set, it applies to every write made through the setter.
B. useSessionStorage
Saves a value in sessionStorage. The value persists through page reloads but only within the current tab. It is removed when the tab is closed.
function ProductDescriptionField() {
const [draft, setDraft, clearDraft] = useSessionStorage('product.draft', '');
const handleSubmit = async () => {
// Submit form...
clearDraft();
};
return (
<textarea
className="form-control"
value={draft}
onChange={e => setDraft(e.target.value)}
/>
);
}
C. useStorage
useLocalStorage and useSessionStorage are shortcuts for the more general useStorage hook. Use it when the storage type is decided at runtime.
import { useStorage } from '@chappy/utils/storage';
const [value, setValue, remove] = useStorage(rememberMe ? 'local' : 'session', 'login.email', '');
D. Keeping Components in Sync
All components that use the same key share the same value. When one component updates it, every other component using that key re-renders automatically.
- Same tab - Works for both
localandsession. - Other tabs - Works for
localonly. Changing a value in one tab updates any open tab using that key.
This also works when values are changed with the utility functions directly. For example, calling local.set('weather.units', 'metric') outside a component updates every component using useLocalStorage('weather.units', ...).
TIP: The initial value is only read on the first render, so it is safe to pass an object or array literal:
const [filters, setFilters] = useSessionStorage('catalog.filters', { genre: 'all', sort: 'name' });
5. Choosing Between Local and Session Storage Table of Contents
local |
session |
|
|---|---|---|
| Survives page reload | Yes | Yes |
| Survives closing the tab | Yes | No |
| Shared across tabs | Yes | No |
| Good for | Preferences, recently viewed items, guest carts | Filters, sort order, form drafts, multi-step wizards |
As a rule of thumb, use local for things the user would expect to be remembered next time they visit, and session for things that only matter while they are working in the current tab.
6. Security Considerations Table of Contents
Data in web storage can be read by any JavaScript running on the page. If your app ever has a cross-site scripting (XSS) vulnerability, everything in storage is exposed. Follow these rules:
- Do not store authentication tokens, session IDs, or CSRF tokens. Chappy.php handles authentication with server-side sessions and cookies, and that should remain the source of truth.
- Do not store passwords or personally identifiable information such as addresses, phone numbers, or payment details.
- Do treat stored values as untrusted input. A user can edit storage in their browser’s developer tools, so validate anything that is sent back to the server.
- Do clear user-specific data when a user logs out, especially on shared computers:
import { local, session } from '@chappy/utils/storage';
local.clear();
session.clear();
7. Testing with Vitest Table of Contents
The jsdom environment used by Vitest provides both localStorage and sessionStorage, so no extra setup is required. Clear storage before each test to keep tests independent.
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { local } from '@chappy/utils/storage';
describe('storage', () => {
beforeEach(() => {
localStorage.clear();
vi.useRealTimers();
});
it('returns stored values', () => {
local.set('units', 'metric');
expect(local.get('units')).toBe('metric');
});
it('returns the fallback for missing keys', () => {
expect(local.get('missing', 'imperial')).toBe('imperial');
});
it('expires values after the TTL', () => {
vi.useFakeTimers();
local.set('temp', 'value', { ttl: 60 });
vi.advanceTimersByTime(61 * 1000);
expect(local.get('temp')).toBeNull();
});
});
To test hooks, render a component with @testing-library/react and wrap updates in act():
import { act, renderHook } from '@testing-library/react';
import { useLocalStorage } from '@chappy/utils/storage';
it('updates the stored value', () => {
const { result } = renderHook(() => useLocalStorage('units', 'imperial'));
act(() => result.current[1]('metric'));
expect(result.current[0]).toBe('metric');
expect(localStorage.getItem('chappy:units')).toContain('metric');
});
To simulate a change from another tab, dispatch a StorageEvent:
act(() => {
localStorage.setItem('chappy:units', JSON.stringify({ value: 'metric' }));
window.dispatchEvent(new StorageEvent('storage', { key: 'chappy:units' }));
});
8. API Reference Table of Contents
Store Methods
Available on both local and session.
| Method | Description |
|---|---|
get(key, fallback = null) |
Returns the stored value, or fallback if missing, malformed, or expired. |
set(key, value, { ttl }) |
Saves a JSON-serializable value. ttl is optional and in seconds. Returns true on success and false on failure. |
remove(key) |
Deletes a single key. |
keys() |
Returns an array of all keys saved by your app in this store. |
clear() |
Deletes all keys saved by your app in this store. |
raw(key) |
Returns the raw stored string without parsing. Mainly used internally by the hooks. |
fullKey(key) |
Returns the key with the prefix applied. |
Store Properties
| Property | Description |
|---|---|
type |
Either 'local' or 'session'. |
persistent |
true when using real browser storage, false when using the in-memory fallback. |
Hooks
| Hook | Description |
|---|---|
useLocalStorage(key, initialValue, { ttl }) |
Binds a component to a localStorage key. Returns [value, setValue, remove]. |
useSessionStorage(key, initialValue, { ttl }) |
Binds a component to a sessionStorage key. Returns [value, setValue, remove]. |
useStorage(type, key, initialValue, { ttl }) |
General form of the hooks above. type is 'local' or 'session'. |
Constants
| Constant | Description |
|---|---|
CHANGE_EVENT |
Name of the window event (chappy:storage) dispatched when a value changes in the current tab. The event’s detail contains { type, key }, where key is null after clear(). |