React Custom Hooks
Custom Hooks ermöglichen es dir, wiederverwendbare Logik zwischen Komponenten zu teilen. Sie sind eine der mächtigsten Features von React!
Was sind Custom Hooks?
// ❌ Code Duplication - Schlecht!
function ComponentA() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [])
// ...
}
function ComponentB() {
// Gleicher Code nochmal! 😫
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
// ...
}
// ✅ Custom Hook - DRY!
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setData(data))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
// Verwenden
function ComponentA() {
const { data, loading, error } = useFetch('/api/users')
// ...
}
function ComponentB() {
const { data, loading } = useFetch('/api/posts')
// ...
}Hook Rules
Wichtig: Custom Hooks müssen diese Regeln befolgen:
- Name muss mit
usebeginnen - Nur in Function Components oder anderen Hooks aufrufen
- Nur auf Top-Level aufrufen (nicht in Loops/Conditions)
// ✅ RICHTIG
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
// ...
return width
}
// ❌ FALSCH - Name beginnt nicht mit "use"
function windowWidth() {
const [width, setWidth] = useState(window.innerWidth)
return width
}
// ❌ FALSCH - In Condition
function Component() {
if (condition) {
const value = useCustomHook() // Error!
}
}
// ✅ RICHTIG
function Component() {
const value = useCustomHook()
if (condition) {
// value verwenden
}
}useFetch - Data Fetching Hook
import { useState, useEffect } from 'react'
function useFetch(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const json = await response.json()
setData(json)
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// Verwenden
function UserList() {
const { data: users, loading, error } = useFetch('/api/users')
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}useLocalStorage - Persistent State
import { useState, useEffect } from 'react'
function useLocalStorage(key, initialValue) {
// State mit Wert aus localStorage oder initialValue
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
// Bei Änderung in localStorage speichern
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(error)
}
}, [key, value])
return [value, setValue]
}
// Verwenden
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme: {theme}
</button>
<button onClick={() => setFontSize(fontSize + 2)}>
Font Size: {fontSize}px
</button>
</div>
)
}
// Theme bleibt gespeichert nach Page Reload! 🎉useToggle - Boolean State
import { useState } from 'react'
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = () => setValue(prev => !prev)
const setTrue = () => setValue(true)
const setFalse = () => setValue(false)
return [value, toggle, setTrue, setFalse]
}
// Verwenden
function Modal() {
const [isOpen, toggle, open, close] = useToggle()
return (
<div>
<button onClick={open}>Open Modal</button>
{isOpen && (
<div className="modal">
<h2>Modal Content</h2>
<button onClick={close}>Close</button>
<button onClick={toggle}>Toggle</button>
</div>
)}
</div>
)
}
// Oder einfacher
function Sidebar() {
const [isOpen, toggle] = useToggle(true)
return (
<aside className={isOpen ? 'open' : 'closed'}>
<button onClick={toggle}>Toggle Sidebar</button>
</aside>
)
}useDebounce - Input Debouncing
import { useState, useEffect } from 'react'
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// Verwenden - Search Component
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearch = useDebounce(searchTerm, 500)
useEffect(() => {
if (debouncedSearch) {
// API Call nur nach 500ms ohne weitere Eingabe
fetch(`/api/search?q=${debouncedSearch}`)
.then(res => res.json())
.then(data => console.log(data))
}
}, [debouncedSearch])
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
)
}
// User tippt "react" -> Wartet 500ms -> Dann API Call
// Spart unnötige API Calls! 🚀useWindowSize - Responsive Hook
import { useState, useEffect } from 'react'
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowSize
}
// Verwenden
function ResponsiveComponent() {
const { width, height } = useWindowSize()
return (
<div>
<p>Window width: {width}px</p>
<p>Window height: {height}px</p>
{width < 768 ? (
<MobileMenu />
) : (
<DesktopMenu />
)}
</div>
)
}useOnClickOutside - Click Detection
import { useEffect } from 'react'
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// Klick innerhalb des Elements? -> Ignorieren
if (!ref.current || ref.current.contains(event.target)) {
return
}
// Klick außerhalb -> Handler aufrufen
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
// Verwenden
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)
useOnClickOutside(dropdownRef, () => setIsOpen(false))
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Dropdown
</button>
{isOpen && (
<div className="dropdown-menu">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
<a href="#">Option 3</a>
</div>
)}
</div>
)
}
// Dropdown schließt sich bei Klick außerhalb! ✨useForm - Form Handling
import { useState } from 'react'
function useForm(initialValues, onSubmit) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const handleChange = (e) => {
const { name, value } = e.target
setValues(prev => ({ ...prev, [name]: value }))
}
const handleSubmit = async (e) => {
e.preventDefault()
setIsSubmitting(true)
setErrors({})
try {
await onSubmit(values)
} catch (error) {
setErrors(error.errors || {})
} finally {
setIsSubmitting(false)
}
}
const reset = () => {
setValues(initialValues)
setErrors({})
}
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit,
reset
}
}
// Verwenden
function LoginForm() {
const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
{ email: '', password: '' },
async (values) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(values)
})
if (!response.ok) throw new Error('Login failed')
}
)
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span>{errors.password}</span>}
<button disabled={isSubmitting}>
{isSubmitting ? 'Loading...' : 'Login'}
</button>
</form>
)
}useAsync - Async Operations
import { useState, useCallback } from 'react'
function useAsync() {
const [status, setStatus] = useState('idle')
const [value, setValue] = useState(null)
const [error, setError] = useState(null)
const execute = useCallback(async (asyncFunction) => {
setStatus('pending')
setValue(null)
setError(null)
try {
const response = await asyncFunction()
setValue(response)
setStatus('success')
return response
} catch (error) {
setError(error)
setStatus('error')
throw error
}
}, [])
return {
execute,
status,
value,
error,
isIdle: status === 'idle',
isPending: status === 'pending',
isSuccess: status === 'success',
isError: status === 'error'
}
}
// Verwenden
function UserProfile({ userId }) {
const { execute, value: user, isLoading, isError, error } = useAsync()
useEffect(() => {
execute(() => fetch(`/api/users/${userId}`).then(r => r.json()))
}, [userId])
if (isLoading) return <div>Loading...</div>
if (isError) return <div>Error: {error.message}</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}usePrevious - Previous Value
import { useRef, useEffect } from 'react'
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// Verwenden
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<h1>Current: {count}</h1>
<h2>Previous: {prevCount}</h2>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
// Animation Example
function AnimatedValue({ value }) {
const prevValue = usePrevious(value)
const isIncreasing = value > prevValue
return (
<div className={isIncreasing ? 'increase' : 'decrease'}>
{value}
</div>
)
}useMediaQuery - Responsive Design
import { useState, useEffect } from 'react'
function useMediaQuery(query) {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => setMatches(media.matches)
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [matches, query])
return matches
}
// Verwenden
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
return (
<div>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</div>
)
}
// Oder
function Header() {
const isSmallScreen = useMediaQuery('(max-width: 640px)')
return (
<header>
{isSmallScreen ? <MobileMenu /> : <DesktopMenu />}
</header>
)
}📝 Quiz
Was muss bei Custom Hooks IMMER beachtet werden?
Tipps & Tricks
Hook Organisation
src/
├── hooks/
│ ├── useFetch.js
│ ├── useLocalStorage.js
│ ├── useToggle.js
│ ├── useDebounce.js
│ └── index.js // Re-export all
├── components/
└── App.jsx
// hooks/index.js
export { useFetch } from './useFetch'
export { useLocalStorage } from './useLocalStorage'
export { useToggle } from './useToggle'
// In Component
import { useFetch, useToggle } from './hooks'
TypeScript Support
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
})
return [value, setValue] as const
}
// Verwenden
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
Composition von Hooks
// Mehrere Hooks kombinieren
function useUserData(userId) {
const { data, loading } = useFetch(\`/api/users/\${userId}\`)
const [favorites, setFavorites] = useLocalStorage('favorites', [])
const isOnline = useOnlineStatus()
return {
user: data,
loading,
favorites,
setFavorites,
isOnline
}
}
Cleanup Functions
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay)
return () => clearInterval(id) // Cleanup!
}, [callback, delay])
}
Häufige Fehler
Fehler 1: Kein "use" Prefix
❌ FALSCH:
function windowWidth() {
const [width, setWidth] = useState(window.innerWidth)
return width
}
✅ RICHTIG:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
return width
}
Fehler 2: Hook in Condition
❌ FALSCH:
function Component() {
if (condition) {
const data = useFetch('/api/data') // Error!
}
}
✅ RICHTIG:
function Component() {
const data = useFetch(condition ? '/api/data' : null)
}
Fehler 3: Dependency Array vergessen
❌ FALSCH:
function useFetch(url) {
useEffect(() => {
fetch(url).then(...)
}) // Missing dependency array!
}
✅ RICHTIG:
function useFetch(url) {
useEffect(() => {
fetch(url).then(...)
}, [url]) // ✅
}
Fehler 4: Kein Cleanup
❌ FALSCH:
function useInterval(callback, delay) {
useEffect(() => {
setInterval(callback, delay)
// Kein cleanup! Memory Leak! 💥
}, [])
}
✅ RICHTIG:
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay)
return () => clearInterval(id) // ✅
}, [callback, delay])
}
Zusammenfassung
Du hast gelernt:
- ✅ Custom Hooks für wiederverwendbare Logik
- ✅ Hook Rules: "use" Prefix, Top-Level
- ✅ useFetch für Data Fetching
- ✅ useLocalStorage für Persistenz
- ✅ useToggle für Boolean State
- ✅ useDebounce für Input Optimization
- ✅ useWindowSize für Responsive Design
- ✅ useOnClickOutside für Click Detection
- ✅ useForm für Form Handling
Key Takeaways:
- Custom Hooks = Wiederverwendbare Logik
- Name MUSS mit "use" beginnen
- Können andere Hooks nutzen
- Nur in Components/Hooks aufrufen
- Nicht in Conditions/Loops
- Cleanup Functions wichtig
- Composition möglich
Common Use Cases:
- Data Fetching
- Form Handling
- Local Storage
- Event Listeners
- Window Events
- Timers/Intervals
- Animation
- Media Queries
Best Practices:
- Eigenen hooks/ Ordner erstellen
- Kleine, fokussierte Hooks
- TypeScript für Type Safety
- Tests schreiben
- Dokumentieren
Praktische Übungen
Übung 1: useCounter Hook
Erstelle einen Counter Hook mit:
- increment()
- decrement()
- reset()
- setCount()
Übung 2: useArray Hook
Implementiere Array Helper:
- push(item)
- remove(index)
- filter(callback)
- clear()
Übung 3: useCopyToClipboard
Baue einen Hook zum Kopieren:
- copy(text)
- isCopied State
- Reset nach 2 Sekunden
Übung 4: useInfiniteScroll
Implementiere Infinite Scroll:
- Detect Scroll Bottom
- Load More Items
- Loading State
Übung 5: useKeyPress
Erstelle Keyboard Hook:
- useKeyPress('Escape')
- useKeyPress('Enter')
- Callback ausführen
Gut gemacht! 🎉
Du hast "React Custom Hooks - Wiederverwendbare Logik" abgeschlossen
Artikel bewerten
Bitte einloggen um zu bewerten
Das könnte dich auch interessieren
React Context API - Globaler State ohne Props Drilling
Lerne die React Context API kennen und vermeide Props Drilling. Teile State einfach zwischen Komponenten ohne Redux!
React useEffect Hook - Side Effects & Lifecycle
Lerne den useEffect Hook für Side Effects, Data Fetching, Subscriptions und Component Lifecycle in React Function Components.
React useRef - Refs und DOM Manipulation
Lerne useRef für DOM-Zugriff, uncontrolled Forms, Animations und persistente Werte die kein Re-Render auslösen!