Beispiel mit LocalStorage
Die Datenpersistenz in React-Anwendungen stellt Entwickler oft vor Herausforderungen. Custom Hooks bieten eine elegante Lösung für die effiziente Verwaltung von LocalStorage-Daten. Dieser Beitrag demonstriert anhand eines praktischen Beispiels, wie ein maßgeschneiderter Hook die Browser-Speicherung vereinfacht, den Code besser strukturiert und die Wiederverwendbarkeit fördert. Der Leitfaden zeigt ein effektives Implementierungsmuster für die nahtlose Integration von LocalStorage in moderne React-Anwendungen und erläutert dessen Anwendung im Entwicklungsalltag.
Inhaltsverzeichnis
Beschreibung
In diesem Beispiel möchten wir einen Custom Hook definieren, welcher eine Box mit Funktionalität für das Schreiben, Lesen und Löschen aus dem LocalStorage darstellt.
Sprich, mit diesem Custom Hook wollen wir in der Lage sein, beliebige Werte in LocalStorage speichern zu können. Damit können wir diesen Hook an unterschiedlichen Stellen unserer Anwendung verwenden.
Nach dem wir unseren Custom Hook aufgebaut haben, machen wir eine kleine Komponente, welche Aufgabenliste in LocalStorage verwaltet. Dank der Nutzung von LocalStorage, bleiben unsere gespeicherte Werte auch nach dem vollständigen Neuladen der Browser-Seite erhalten.
Custom Hook für LocalStorage
In diesem Schritt werden wir unseren Custom Hook in einer Datei namens useLocalStorage.js
aufbauen.
Unsere Hook-Funktion wird zwei Parameter annehmen:
key
: Schlüssel, unter welchem ein Wert in LocalStorage gespeichert werden soll.initialValue
: Wert, welcher unter dem angegebenen Schlüssel gespeichert werden soll.
Die ausführliche Beschreibung der Funktionalität und der Bestandteile findet ihr weiter unten, nach dem Code.
import { useRef, useState, useCallback } from 'react';
function useLocalStorage(key, initialValue) {
// Prüfen, ob localStorage verfügbar
const isLocalStorageAvailable = typeof window !== 'undefined' && window.localStorage;
// Fallback Speicher (In-Memory)
const memoryStorage = useRef({});
// Lesen eines Wertes
const getStorageItem = () => {
if (isLocalStorageAvailable) {
// Aus localStorage lesen
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(`Fehler beim Lesen von localStorage von "${key}":`, error);
return initialValue;
}
} else {
// Aus dem Speicher lesen
return memoryStorage.current[key] !== undefined ? memoryStorage.current[key] : initialValue;
}
};
// Lazy State-Initialisierung
const [storedValue, setStoredValue] = useState(getStorageItem);
// Schreiben des Wertes
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (isLocalStorageAvailable) {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
window.dispatchEvent(new CustomEvent('local-storage', {
detail: { key: key, newValue, valueToStore }
}));
} else {
memoryStorage.current[key] = valueToStore;
}
} catch (error) {
console.log(`Fehler beim Speichern in localStorage von "${key}":`, error);
}
}, [
key,
storedValue,
isLocalStorageAvailable
]);
// Löschen eines Wertes
const removeValue = useCallback(() => {
try {
if (isLocalStorageAvailable) {
window.localStorage.removeItem(key);
window.dispatchEvent(
new CustomEvent(
'local-storage',
{ detail: key: key, newValue: null }
)
);
} else {
delete memoryStorage.current[key];
}
setStoredValue(initialValue);
} catch (error) {
console.log(`Fehler beim Löschen von localStorage von "${key}":`, error);
}
}, [
key,
initialValue,
isLocalStorageAvailable
]);
// Listener für native 'storage' Event und CustomEvent
useEffect(() => {
// Handler für navite 'storage' Events
const handleNativeStorage = (event) => {
if (event.storageArea === window.localStorage && event.key === key) {
try {
const newValue = event.newValue ? JSON.parse(event.newValue) : null;
setStoredValue(newValue !== null ? newValue : initialValue);
} catch (error) {
console.log('Fehler in handleNativeStorage', error);
setStoredValue(initialValue);
}
}
};
// Handler für CustomEvent
const handleCustomEvent = (event) => {
if (event.detail?.key === key) {
setStoredValue(event.detail.newValue !== null ? event.detail.newValue : initialValue);
}
};
if (isLocalStorageAvailable) {
window.addEventListener('storage', handleNativeStorage);
window.addEventListener('local-storage', handleCustomEvent);
return () => {
window.removeEventListener('storage', handleNativeStorage);
window.removeEventListener('local-storage', handleCustomEvent);
};
}
}, [
key,
initialValue,
isLocalStorageAvailable
]);
return [storedValue, setValue, removeValue];
}
export default useLocalStorage;
Damit haben wir unseren Custom Hook aufgebaut.
Detaillierte Beschreibung
Nun gehe ich auf einzelne Abschnitte ein, um die Funktion dieser etwas zu erklären.
const isLocalStorageAvailable = typeof window !== 'undefined' && window.localStorage;
- Prüft, ob wir in einer Browser-Umgebung sind (
window
existiert) - Prüft, ob
localStorage
aufwindow
existiert. Wichtig, weil z.B. Server-Side-Rendering (SSR) keinwindow
hat isLocalStorageAvailable
ist ein Boolean, ob localStorage benutzt werden kann
const memoryStorage = useRef({});
- Erzeugt eine Referenz mit
useRef
, die ein Objekt hält - Dieses Objekt ist ein einfacher, temporärer Speicher, wenn
localStorage
nicht verfügbar ist useRef
sorgt dafür, dass sich das Objekt über mehrere Render-Zyklen nicht verändert- Das
memoryStorage
ist nur im aktuellen Tab und während der Session gültig
const getStorageItem = () => {
if (isLocalStorageAvailable) {
// Aus localStorage lesen
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(`Fehler beim Lesen von localStorage von "${key}":`, error);
return initialValue;
}
} else {
// Aus dem Speicher lesen
return memoryStorage.current[key] !== undefined ? memoryStorage.current[key] : initialValue;
}
};
- Wenn localStorage verfügbar ist:
- Versuche den Wert zu lesen (
getItem(key)
), das gibt einen String odernull
- Wenn es einen Wert gibt (
item
nichtnull
), parse ihn mitJSON.parse
, damit auch Objekte oder Arrays erhalten bleiben - Falls kein Wert da ist (
null
), gibinitialValue
zurück - Sonst fange Fehler ab (z.B. ungültiges JSON) und gib dann
initialValue
zurück
- Versuche den Wert zu lesen (
- Wenn localStorage nicht verfügbar ist:
- Greife auf das In-Memory-Objekt
memoryStorage.current
zu - Wenn der Schlüssel dort existiert, gib den Wert zurück
- Sonst gib
initialValue
zurück
- Greife auf das In-Memory-Objekt
const [storedValue, setStoredValue] = useState(getStorageItem);
- Zustandswerte, um den aktuellen Wert zu halten
- Die Funktion
getStorageItem
wird lazy nur beim ersten Render ausgeführt, um initialen Wert zu setzen storedValue
ist die lokale State-VariablesetStoredValue
ist die Funktion zum Aktualisieren
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (isLocalStorageAvailable) {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
window.dispatchEvent(new CustomEvent('local-storage', {
detail: { key: key, newValue, valueToStore }
}));
} else {
memoryStorage.current[key] = valueToStore;
}
} catch (error) {
console.log(`Fehler beim Speichern in localStorage von "${key}":`, error);
}
}, [
key,
storedValue,
isLocalStorageAvailable
]);
Dies ist die Funktion, die verwendet wird, um einen Wert in localStorage zu speichern.
useCallback
sorgt dafür, dass die Funktion nicht bei jedem Render neu erzeugt wird, sondern nur wenn sich key
, storedValue
oder isLocalStorageAvailable
ändern.
value
kann entweder ein Wert sein (z.B."dark"
) oder eine Funktion, die den aktuellen Wert nimmt und einen neuen Wert zurückgibt (wie inuseState
üblich).- Falls
value
eine Funktion ist, wird sie mit dem aktuellenstoredValue
aufgerufen, um den neuen Wert zu berechnen - Dann wird
setStoredValue
ausgeführt, um den React State zu aktualisieren - Falls localStorage verfügbar ist:
- Der neue Wert wird als JSON-String in localStorage gespeichert (
setItem
) - Ein CustomEvent
local-storage
wird ausgelöst, um andere Hook-Instanzen im gleichen Tab über die Änderung zu informieren
- Der neue Wert wird als JSON-String in localStorage gespeichert (
- Wenn kein localStorage verfügbar ist:
- Der Wert wird im In-Memory-Objekt gespeichert
const removeValue = useCallback(() => {
try {
if (isLocalStorageAvailable) {
window.localStorage.removeItem(key);
window.dispatchEvent(
new CustomEvent(
'local-storage',
{ detail: key: key, newValue: null }
)
);
} else {
delete memoryStorage.current[key];
}
setStoredValue(initialValue);
} catch (error) {
console.log(`Fehler beim Löschen von localStorage von "${key}":`, error);
}
}, [
key,
initialValue,
isLocalStorageAvailable
]);
Dies ist die Funktion, die einen gespeicherten Wert entfernt.
- Falls localStorage vorhanden:
- Entfernt den Eintrag via
removeItem(key)
- Sendet ebenfalls das
local-storage
Event mitnewValue: null
, damit alle Listener reagieren
- Entfernt den Eintrag via
- Falls localStorage nicht vorhanden:
- Löscht den Eintrag im Fallback-Speicher
- Setzt State zurück auf
initialValue
useCallback
sorgt für stabile Funktion, die nur dann neu erzeugt wird, wennkey
,initialValue
oderisLocalStorageAvailable
sich ändern
TodoComponent
Nun bauen wir das Todo-Component auf und werden dort unseren Custom Hook useLocalStorage
verwenden. Die Todo-Elemente werden wir im localStorage-Speichern speichern.
Wir fügen ebenfalls Stile in diesem Component hinzu, damit wir ein wenig das Aussehen optimieren. Diese haben, wie immer, keinen Zusammenhang mit dem Thema an sich.
import { useState } from 'react';
import useLocalStorage from './useLocalStorage';
function ExampleLocalStorage() {
// Verwendung - Custom Hook
const [todos, setTodos] = useLocalStorage('todos', []);
// Verwendung - State
const [inputValue, setInputValue] = useState('');
// Handler - Todo hinzufügen
const handleAddTodo = () => {
if (inputValue.trim()) {
const newTodo = {
id: Date.now(),
text: inputValue,
completed: false,
createdAt: new Date().toISOString()
};
setTodos(prevTodos => [...prevTodos, newTodo]);
setInputValue('');
}
};
// Handler - Todo aktivieren/deaktivieren
const handleToggleTodo = id => {
setTodos(prevTodos => (prevTodos.map(todo => (
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))));
};
// Handler - Todo entfernen
const handleDeleteTodo = id => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
const containerStyle = {
maxWidth: '500px',
margin: '20px auto',
padding: 20,
border: '2px solid #dddddd',
borderRadius: 8
};
const inputContainerStyle = {
display: 'flex',
marginBottom: 20
};
const inputStyle = {
flex: 1,
padding: 10,
fontSize: 16,
border: '2px solid #cccccc',
borderRadius: '4px 0 0 4px'
};
const addButtonStyle = {
padding: '10px 20px',
backgroundColor: '#28a745',
color: '#ffffff',
border: 'none',
borderRadius: '0 4px 4px 0',
cursor: 'pointer',
fontSize: 16
};
const todoItemStyle = {
display: 'flex',
alignItems: 'center',
padding: 10,
borderBottom: '2px solid #eeeeee'
};
const todoTextStyle = (completed) => ({
flex: 1,
textDecoration: completed ? 'line-through' : 'none',
opacity: completed ? 0.6 : 1,
cursor: 'pointer'
});
const deleteButtonStyle = {
padding: '5px 10px',
backgroundColor: '#dc3545',
color: '#ffffff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 12
};
return (
<div style={containerStyle}>
<div style={inputContainerStyle}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyUp={(e) => e.key === 'Enter' && addTodo()}
style={inputStyle}
placeholder="Neue Aufgabe ..."
/>
<button style={addButtonStyle} onClick={addTodo}>
Hinzufügen
</button>
</div>
<div>
{todos.length === 0 ? (
<p style={{ textAlign: 'center', color: '#666666' }}>
Keine Aufgaben vorhanden. Füge eine hinzu!
</p>
) : (
todos.map(todo => (
<div key={todo.id} style={todoItemStyle}>
<span
onClick={() => toggleTodo(todo.id)}
style={todoTextStyle(todo.completed)}
>
{todo.text}
</span>
<button
style={deleteButtonStyle}
onClick={() => deleteTodo(todo.id)}
>
Löschen
</button>
</div>
))
)}
</div>
{todos.length > 0 && (
<p style={{ marginTop: 10, fontSize: 14, color: '#666666' }}>
{todos.filter(t => t.completed).length} von {todos.length} erledigt.
</p>
)}
</div>
);
}
export default ExampleLocalStorage;
Damit haben wir unser Component und unsere kleine Todo-App aufgebaut, in der wir unseren Custom Hook useLocalStorage
verwenden.
Wenn wir in unserer Anwendung nun ein paar Aufgaben hinzufügen, bestimmte anklicken, um diese als erledigt zu markieren und einen Blick in den LocalStorage-Speicher werfen, werden wir feststellen, dass alle unseren erstellten Aufgaben dort zu finden sind. Auch, wenn wir die Seite vollständig neuladen, bleiben die Aufgaben erhalten und werden sofort wieder in die App hineingeladen.