React LocalStorageの活用|データを永続化する簡単な方法
React でLocalStorageを使ったデータ永続化の実装方法を解説。基本的な使い方からカスタムHooksまで、実践的なサンプルコードとともに詳しく紹介します。
「ページをリロードしたら、入力したデータが全部消えた...」
こんな経験、ありませんか?
「ユーザーの設定を保存したい」 「ログイン状態を維持したい」 「入力途中のフォームデータを残したい」
Reactでアプリを作っていると、こういう場面によく出会いますよね。 実は、LocalStorageを使えばデータを簡単に保存できるんです。
この記事では、ReactでLocalStorageを使う方法を分かりやすく解説します。 基本的な使い方から実用的なカスタムHookまで、実際に動くサンプルコードで学べますよ。
読み終わる頃には、あなたも「データが消えない便利なアプリ」が作れるようになります!
LocalStorageって何?
「LocalStorageって聞いたことあるけど、よく分からない...」 そんな方も大丈夫です。
簡単に言うとブラウザの保存箱
LocalStorageは、ブラウザが用意してくれるデータ保存スペースです。
// データを保存
localStorage.setItem('username', 'たろう');
// データを取得
const username = localStorage.getItem('username');
console.log(username); // "たろう"
// データを削除
localStorage.removeItem('username');
// 全部削除
localStorage.clear();
この4つの操作だけで基本は完了です!
setItem()
→ データを保存getItem()
→ データを取得removeItem()
→ データを削除clear()
→ 全部削除
思っているより簡単ですよね。
他の保存方法との違いは?
「Cookieとか、SessionStorageとかもあるけど、何が違うの?」
const storageComparison = {
localStorage: {
容量: "5-10MB",
保存期間: "手動で削除するまで永続",
範囲: "同じサイト内",
使いやすさ: "簡単"
},
sessionStorage: {
容量: "5-10MB",
保存期間: "タブを閉じるまで",
範囲: "同じタブ内",
使いやすさ: "簡単"
},
cookie: {
容量: "4KBまで",
保存期間: "設定した期限まで",
範囲: "同じサイト内",
使いやすさ: "ちょっと面倒"
}
};
LocalStorageの良いところ
- 容量が大きい(5-10MB)
- 使い方が簡単
- データがずっと残る
- 高速でアクセスできる
一般的なWebアプリなら、LocalStorageで十分です。
いつ使うのがおすすめ?
こんな時にLocalStorageが活躍します
- ユーザー設定(テーマ、言語など)
- ショッピングカートの内容
- フォームの入力途中データ
- ゲームのスコア
- To-doリストの内容
使わない方がいいもの
- パスワード → セキュリティ上危険
- クレジットカード番号 → 絶対ダメ
- 個人情報 → 慎重に扱う
安全で便利なデータだけ保存しましょう。
ReactでLocalStorageを使ってみよう
実際にReactでLocalStorageを使ってみましょう。 最初は簡単な例から始めます。
カウンターアプリで試してみる
まずは、数字をカウントするアプリを作ってみます。
import React, { useState, useEffect } from 'react';
const PersistentCounter = () => {
// LocalStorageから初期値を取得
const [count, setCount] = useState(() => {
const savedCount = localStorage.getItem('counter');
return savedCount ? parseInt(savedCount, 10) : 0;
});
// countが変わったらLocalStorageに保存
useEffect(() => {
localStorage.setItem('counter', count.toString());
}, [count]);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>永続化カウンター</h2>
<p style={{ fontSize: '24px' }}>現在の値: {count}</p>
<div>
<button onClick={increment} style={{ margin: '5px', padding: '10px' }}>
+1
</button>
<button onClick={decrement} style={{ margin: '5px', padding: '10px' }}>
-1
</button>
<button onClick={reset} style={{ margin: '5px', padding: '10px' }}>
リセット
</button>
</div>
<p style={{ fontSize: '12px', color: '#666' }}>
ページをリロードしても値が保持されます
</p>
</div>
);
};
このコードのポイント
useState(() => {...})
→ 初期値をLocalStorageから取得useEffect(() => {...}, [count])
→ countが変わったら保存parseInt(savedCount, 10)
→ 文字列を数字に変換
なぜこの順序なの?
LocalStorageは文字列しか保存できません。 数字を保存する時は、文字列に変換して保存します。 取得する時は、文字列を数字に戻します。
実際に試してみましょう
- +1ボタンを何回か押す
- ブラウザをリロード(F5キー)
- 数字がそのまま残っている!
これだけで、データが永続化されました。
オブジェクトを保存してみる
今度は、もう少し複雑なデータを保存してみましょう。
import React, { useState, useEffect } from 'react';
const UserProfile = () => {
const [user, setUser] = useState(() => {
const savedUser = localStorage.getItem('userProfile');
return savedUser ? JSON.parse(savedUser) : {
name: '',
email: '',
age: '',
preferences: {
theme: 'light',
notifications: true
}
};
});
// ユーザー情報が変わったら保存
useEffect(() => {
localStorage.setItem('userProfile', JSON.stringify(user));
}, [user]);
const updateUser = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
const updatePreferences = (field, value) => {
setUser(prev => ({
...prev,
preferences: {
...prev.preferences,
[field]: value
}
}));
};
const clearProfile = () => {
setUser({
name: '',
email: '',
age: '',
preferences: {
theme: 'light',
notifications: true
}
});
localStorage.removeItem('userProfile');
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
<h2>ユーザープロフィール</h2>
<div style={{ marginBottom: '15px' }}>
<label>
名前:
<input
type="text"
value={user.name}
onChange={(e) => updateUser('name', e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
メールアドレス:
<input
type="email"
value={user.email}
onChange={(e) => updateUser('email', e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
年齢:
<input
type="number"
value={user.age}
onChange={(e) => updateUser('age', e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
テーマ:
<select
value={user.preferences.theme}
onChange={(e) => updatePreferences('theme', e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
>
<option value="light">ライト</option>
<option value="dark">ダーク</option>
</select>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
<input
type="checkbox"
checked={user.preferences.notifications}
onChange={(e) => updatePreferences('notifications', e.target.checked)}
style={{ marginRight: '10px' }}
/>
通知を受け取る
</label>
</div>
<button onClick={clearProfile} style={{ padding: '10px', marginTop: '10px' }}>
プロフィールをクリア
</button>
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f0f0' }}>
<h3>保存されたデータ:</h3>
<pre>{JSON.stringify(user, null, 2)}</pre>
</div>
</div>
);
};
オブジェクト保存のポイント
JSON.stringify()
→ オブジェクトを文字列に変換JSON.parse()
→ 文字列をオブジェクトに戻す- スプレッド演算子
...
→ オブジェクトの一部だけ更新
どうやって動いているの?
// 保存の流れ
const user = { name: "太郎", age: 25 };
const jsonString = JSON.stringify(user); // '{"name":"太郎","age":25}'
localStorage.setItem('user', jsonString);
// 取得の流れ
const jsonString = localStorage.getItem('user'); // '{"name":"太郎","age":25}'
const user = JSON.parse(jsonString); // { name: "太郎", age: 25 }
JSONを使うことで、複雑なデータも簡単に保存できます。
カスタムHookで使いやすくしよう
「毎回useState とuseEffect を書くのは面倒...」
そんな時は、カスタムHookを作りましょう。 一度作れば、どこでも簡単に使えるようになります。
基本的なuseLocalStorageを作ろう
import { useState, useEffect } from 'react';
const useLocalStorage = (key, initialValue) => {
// LocalStorageから値を取得する関数
const getStoredValue = () => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
};
// 状態を初期化
const [storedValue, setStoredValue] = useState(getStoredValue);
// 値を設定する関数
const setValue = (value) => {
try {
// 関数が渡された場合は実行
const valueToStore = value instanceof Function ? value(storedValue) : value;
// 状態を更新
setStoredValue(valueToStore);
// LocalStorageに保存
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
// 値を削除する関数
const removeValue = () => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
};
return [storedValue, setValue, removeValue];
};
このカスタムHookの便利なところ
- エラーハンドリングが組み込まれている
- 削除機能もついている
- useState と同じように使える
使い方はこんなに簡単
const TodoApp = () => {
const [todos, setTodos, removeTodos] = useLocalStorage('todos', []);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: inputValue,
completed: false,
createdAt: new Date().toISOString()
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>永続化Todoアプリ</h2>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新しいタスクを入力"
style={{ padding: '10px', width: '300px' }}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo} style={{ padding: '10px', marginLeft: '10px' }}>
追加
</button>
</div>
<div style={{ marginBottom: '20px' }}>
<button onClick={removeTodos} style={{ padding: '10px', backgroundColor: '#dc3545', color: 'white' }}>
全て削除
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li key={todo.id} style={{
display: 'flex',
alignItems: 'center',
padding: '10px',
marginBottom: '10px',
backgroundColor: todo.completed ? '#f0f0f0' : 'white',
border: '1px solid #ddd',
borderRadius: '4px'
}}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: '10px' }}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none',
flex: 1
}}>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
style={{
padding: '5px 10px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer'
}}
>
削除
</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p style={{ textAlign: 'center', color: '#666' }}>
タスクがありません
</p>
)}
</div>
);
};
カスタムHookを使うメリット
- コードがスッキリする
- 再利用できる
- エラー処理を気にしなくていい
- 削除機能も簡単に使える
たった3行でLocalStorage機能が使えるようになりました!
もっと高機能なHookを作ってみる
「有効期限をつけたい」「他のタブと同期したい」
そんな高度な要求にも対応できるHookを作ってみましょう。
import { useState, useEffect, useCallback } from 'react';
const useAdvancedLocalStorage = (key, initialValue, options = {}) => {
const {
serializer = JSON,
syncAcrossTabs = true,
expiration = null // ミリ秒単位での有効期限
} = options;
// シリアライザーのデフォルト実装
const { stringify, parse } = serializer;
// 有効期限付きのデータ構造
const createStorageValue = (value) => ({
value,
timestamp: Date.now(),
expiration: expiration ? Date.now() + expiration : null
});
// LocalStorageから値を取得
const getStoredValue = useCallback(() => {
try {
const item = window.localStorage.getItem(key);
if (!item) return initialValue;
const parsed = parse(item);
// 有効期限チェック
if (parsed.expiration && Date.now() > parsed.expiration) {
window.localStorage.removeItem(key);
return initialValue;
}
return parsed.value;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
}, [key, initialValue, parse]);
const [storedValue, setStoredValue] = useState(getStoredValue);
// 値を設定
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
const storageData = createStorageValue(valueToStore);
setStoredValue(valueToStore);
window.localStorage.setItem(key, stringify(storageData));
// カスタムイベントを発火(他のタブとの同期用)
if (syncAcrossTabs) {
window.dispatchEvent(new CustomEvent('localStorageChange', {
detail: { key, value: valueToStore }
}));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue, stringify, syncAcrossTabs]);
// 値を削除
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
// 他のタブとの同期
useEffect(() => {
if (!syncAcrossTabs) return;
const handleStorageChange = (e) => {
if (e.key === key) {
setStoredValue(getStoredValue());
}
};
const handleCustomStorageChange = (e) => {
if (e.detail.key === key) {
setStoredValue(e.detail.value);
}
};
window.addEventListener('storage', handleStorageChange);
window.addEventListener('localStorageChange', handleCustomStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('localStorageChange', handleCustomStorageChange);
};
}, [key, getStoredValue, syncAcrossTabs]);
return [storedValue, setValue, removeValue];
};
この高機能Hookでできること
- 有効期限設定 → 一定時間後にデータを自動削除
- タブ間同期 → 複数のタブでデータを共有
- エラー処理 → 問題が起きても安全に動作
使用例:設定画面
const SettingsApp = () => {
const [theme, setTheme] = useAdvancedLocalStorage('theme', 'light');
const [language, setLanguage] = useAdvancedLocalStorage('language', 'ja');
// 1時間で期限切れの一時データ
const [tempData, setTempData] = useAdvancedLocalStorage('tempData', '', {
expiration: 60 * 60 * 1000 // 1時間
});
return (
<div style={{ padding: '20px' }}>
<h2>設定画面</h2>
<div style={{ marginBottom: '20px' }}>
<h3>テーマ設定</h3>
<label>
<input
type="radio"
value="light"
checked={theme === 'light'}
onChange={(e) => setTheme(e.target.value)}
/>
ライトテーマ
</label>
<label style={{ marginLeft: '20px' }}>
<input
type="radio"
value="dark"
checked={theme === 'dark'}
onChange={(e) => setTheme(e.target.value)}
/>
ダークテーマ
</label>
</div>
<div style={{ marginBottom: '20px' }}>
<h3>一時データ(1時間で期限切れ)</h3>
<input
type="text"
value={tempData}
onChange={(e) => setTempData(e.target.value)}
placeholder="一時的なメモ"
style={{ width: '100%', padding: '10px' }}
/>
</div>
</div>
);
};
カスタムHookの組み合わせ技
基本版と高機能版を使い分けることで、シンプルさと機能性の両立ができます。
実用的なアプリを作ってみよう
理論だけじゃつまらないですよね。 実際に使えるショッピングカートアプリを作ってみましょう。
ショッピングカート機能
import React, { useState } from 'react';
const useShoppingCart = () => {
const [cart, setCart] = useLocalStorage('shoppingCart', []);
const addItem = (product) => {
setCart(prev => {
const existingItem = prev.find(item => item.id === product.id);
if (existingItem) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const removeItem = (productId) => {
setCart(prev => prev.filter(item => item.id !== productId));
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeItem(productId);
return;
}
setCart(prev => prev.map(item =>
item.id === productId
? { ...item, quantity }
: item
));
};
const clearCart = () => {
setCart([]);
};
const getTotalPrice = () => {
return cart.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getTotalItems = () => {
return cart.reduce((total, item) => total + item.quantity, 0);
};
return {
cart,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems
};
};
const ShoppingApp = () => {
const {
cart,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems
} = useShoppingCart();
// サンプル商品データ
const products = [
{ id: 1, name: 'MacBook Pro', price: 200000, image: '💻' },
{ id: 2, name: 'iPhone', price: 100000, image: '📱' },
{ id: 3, name: 'AirPods', price: 30000, image: '🎧' },
{ id: 4, name: 'iPad', price: 60000, image: '📱' },
{ id: 5, name: 'Apple Watch', price: 50000, image: '⌚' }
];
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
<h1>オンラインストア</h1>
<div style={{ display: 'flex', gap: '20px' }}>
{/* 商品一覧 */}
<div style={{ flex: 2 }}>
<h2>商品一覧</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '20px'
}}>
{products.map(product => (
<div key={product.id} style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
textAlign: 'center'
}}>
<div style={{ fontSize: '48px', marginBottom: '10px' }}>
{product.image}
</div>
<h3>{product.name}</h3>
<p style={{ fontSize: '18px', fontWeight: 'bold' }}>
¥{product.price.toLocaleString()}
</p>
<button
onClick={() => addItem(product)}
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
カートに追加
</button>
</div>
))}
</div>
</div>
{/* ショッピングカート */}
<div style={{ flex: 1 }}>
<div style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
position: 'sticky',
top: '20px'
}}>
<h2>ショッピングカート ({getTotalItems()})</h2>
{cart.length === 0 ? (
<p style={{ textAlign: 'center', color: '#666' }}>
カートは空です
</p>
) : (
<>
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{cart.map(item => (
<div key={item.id} style={{
display: 'flex',
alignItems: 'center',
padding: '10px',
borderBottom: '1px solid #eee',
marginBottom: '10px'
}}>
<span style={{ fontSize: '24px', marginRight: '10px' }}>
{item.image}
</span>
<div style={{ flex: 1 }}>
<h4 style={{ margin: 0 }}>{item.name}</h4>
<p style={{ margin: 0, color: '#666' }}>
¥{item.price.toLocaleString()}
</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
style={{
width: '30px',
height: '30px',
border: '1px solid #ddd',
background: 'white',
cursor: 'pointer'
}}
>
-
</button>
<span style={{ minWidth: '20px', textAlign: 'center' }}>
{item.quantity}
</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
style={{
width: '30px',
height: '30px',
border: '1px solid #ddd',
background: 'white',
cursor: 'pointer'
}}
>
+
</button>
<button
onClick={() => removeItem(item.id)}
style={{
padding: '5px 10px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer'
}}
>
削除
</button>
</div>
</div>
))}
</div>
<div style={{ marginTop: '20px', paddingTop: '20px', borderTop: '2px solid #ddd' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: '18px',
fontWeight: 'bold'
}}>
<span>合計:</span>
<span>¥{getTotalPrice().toLocaleString()}</span>
</div>
<button
style={{
width: '100%',
padding: '15px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '16px',
cursor: 'pointer',
marginTop: '10px'
}}
onClick={() => alert('購入機能はデモです')}
>
購入する
</button>
<button
onClick={clearCart}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
}}
>
カートをクリア
</button>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
};
このショッピングカートの便利機能
- 商品追加 → カートに商品を追加
- 数量変更 → +/-ボタンで数量調整
- 商品削除 → 不要な商品を削除
- 合計計算 → 金額と個数を自動計算
- データ永続化 → ページリロードしても内容保持
実際に試してみてください
- 商品をカートに追加
- 数量を変更
- ブラウザをリロード
- カートの内容がそのまま残っている!
これで実用的なECサイトの基本機能ができました。
このアプリから学べること
データの構造化
const cartItem = {
id: 1, // 商品ID
name: 'iPhone', // 商品名
price: 100000, // 価格
image: '📱', // 画像(絵文字)
quantity: 2 // 数量
};
データを構造化することで、管理しやすくなります。
配列操作のパターン
find()
→ 特定の商品を探すmap()
→ 数量を更新filter()
→ 商品を削除reduce()
→ 合計を計算
これらのパターンは、他のアプリでも応用できます。
エラー対策とセキュリティ
LocalStorageを使う時に気をつけるべきことを学びましょう。
エラーが起きても大丈夫にしよう
「LocalStorageが使えない環境があるの?」
実は、以下の場合にエラーが起きることがあります。
- プライベートブラウジングモード
- 容量不足
- LocalStorage無効設定
- 古いブラウザ
const useRobustLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
// LocalStorageが使えるかチェック
if (typeof window === 'undefined') {
return initialValue;
}
try {
// LocalStorageのサポート確認
if (!window.localStorage) {
console.warn('LocalStorage is not supported');
return initialValue;
}
// 実際に書き込みテスト
const testKey = '__localStorage_test__';
window.localStorage.setItem(testKey, 'test');
window.localStorage.removeItem(testKey);
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('LocalStorage error:', error);
// 容量不足の場合
if (error.name === 'QuotaExceededError') {
console.error('LocalStorage quota exceeded');
clearOldData(); // 古いデータを削除
}
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
setValue(newValue);
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(key, JSON.stringify(newValue));
}
} catch (error) {
console.error('Error saving to localStorage:', error);
if (error.name === 'QuotaExceededError') {
handleQuotaExceeded(); // 容量不足の対処
}
}
};
const clearOldData = () => {
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('temp_') || key.startsWith('old_')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
} catch (error) {
console.error('Error clearing old data:', error);
}
};
return [value, setStoredValue];
};
エラー対策のポイント
- 事前チェック → LocalStorageが使えるか確認
- try-catch → エラーをキャッチして対処
- 容量管理 → 古いデータを自動削除
- フォールバック → 使えない場合は通常のstateで動作
セキュリティに気をつけよう
LocalStorageは便利ですが、セキュリティリスクもあります。
保存してはいけないもの
const securityGuidelines = {
// ❌ 絶対に保存しちゃダメ
avoid: [
'パスワード',
'クレジットカード番号',
'セッショントークン',
'APIキー',
'個人情報(住所、電話番号など)'
],
// ✅ 保存してもOK
safe: [
'ユーザー設定(テーマ、言語)',
'ショッピングカート',
'フォームの下書き',
'ゲームスコア',
'UI状態'
]
};
なぜ危険なの?
- LocalStorageはJavaScriptから誰でもアクセス可能
- XSS攻撃で情報が盗まれる可能性
- 平文で保存されるため暗号化されない
安全に使うコツ
// 機密データは暗号化(簡単な例)
const encryptData = (data) => {
return btoa(JSON.stringify(data)); // Base64エンコード
};
const decryptData = (encryptedData) => {
return JSON.parse(atob(encryptedData)); // Base64デコード
};
// 使用例
const saveUserPreferences = (preferences) => {
const encrypted = encryptData(preferences);
localStorage.setItem('userPrefs', encrypted);
};
ただし、これは簡単な難読化です。 本当に重要なデータは、サーバーで管理しましょう。
パフォーマンスも考えよう
LocalStorageは便利ですが、使いすぎると重くなることがあります。
最適化のコツ
// デバウンス機能付きの保存
const useOptimizedLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
// デバウンス処理(300ms待ってから保存)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, 300);
return () => clearTimeout(timer);
}, [value]);
// LocalStorageへの保存
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(debouncedValue));
} catch (error) {
console.error('LocalStorage save error:', error);
}
}, [key, debouncedValue]);
return [value, setValue];
};
デバウンスって何?
連続した操作をまとめて、最後の操作だけを実行する仕組みです。
// デバウンスなし → 毎回保存(重い)
onChange: (e) => saveToLocalStorage(e.target.value)
// デバウンスあり → 300ms後に一回だけ保存(軽い)
onChange: (e) => setValue(e.target.value) // 300ms後に自動保存
入力のたびに保存せず、少し待ってから保存することでパフォーマンスが向上します。
まとめ:LocalStorageで快適アプリを作ろう
LocalStorageを使えば、ユーザーにとってもっと便利なアプリが作れます。
今回学んだポイント
- 基本操作 → setItem, getItem, removeItem, clear
- React統合 → useState, useEffect との組み合わせ
- カスタムHook → 再利用可能な便利機能
- 実践応用 → ショッピングカート、ユーザー設定
- エラー対策 → 安全で堅牢な実装
- セキュリティ → 適切なデータの選別
すぐに使えるテクニック
- 基本パターンをコピペして使う
- カスタムHookで作業効率アップ
- エラーハンドリングで安定性向上
- セキュリティを意識したデータ選択
LocalStorageが活躍する場面
- ユーザー設定の保存
- ショッピングカート
- フォームの下書き保存
- ゲームスコア
- UI状態の記憶
- 一時的なデータキャッシュ
最後に大切なこと
LocalStorageはユーザー体験を向上させるための道具です。 適切に使えば、ユーザーが「このアプリ使いやすい!」と感じるはずです。
ぜひこの記事を参考にして、実際のプロジェクトでLocalStorageを活用してみてください。 きっと、もっと便利で快適なWebアプリケーションが作れるようになりますよ!