Fortgeschritten182025-01-15

React Custom Hooks - Wiederverwendbare Logik

Lerne Custom Hooks zu erstellen und Code zwischen Komponenten zu teilen. DRY-Prinzip in React mit eigenen Hooks!

#react#hooks#custom-hooks#reusability#clean-code

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?

Custom Hook Konzept
// ❌ 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:

  1. Name muss mit use beginnen
  2. Nur in Function Components oder anderen Hooks aufrufen
  3. Nur auf Top-Level aufrufen (nicht in Loops/Conditions)
Hook Rules
// ✅ 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

useFetch 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

useLocalStorage Hook
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

useToggle Hook
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

useDebounce Hook
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

useWindowSize 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

useOnClickOutside Hook
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

useForm Hook
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

useAsync Hook
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

usePrevious Hook
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

useMediaQuery Hook
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
ReactLektion 9 von 10
90% abgeschlossen
Lektion abgeschlossen!

Gut gemacht! 🎉

Du hast "React Custom Hooks - Wiederverwendbare Logik" abgeschlossen

Artikel bewerten

0.0 (0 Bewertungen)

Bitte einloggen um zu bewerten