React Hooks: Από Basic σε Advanced Χρήση
Tutorials

React Hooks: Από Basic σε Advanced Χρήση

Πλήρης οδηγός για React Hooks - από useState και useEffect μέχρι custom hooks και advanced patterns. Παραδείγματα και best practices.

FutureWebs Team
Συγγραφέας
Δημοσίευση
15 λεπτά
Ανάγνωση

React Hooks: Από Basic σε Advanced Χρήση

Τα React Hooks έφεραν επανάσταση στον τρόπο που γράφουμε React components. Σε αυτόν τον οδηγό θα δούμε από τα βασικά μέχρι τα πιο προχωρημένα patterns.

Basic Hooks

useState - State Management

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const [user, setUser] = useState({ name: '', email: '' })
  
  const increment = () => setCount(prev => prev + 1)
  
  const updateUser = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value }))
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      
      <input 
        value={user.name}
        onChange={(e) => updateUser('name', e.target.value)}
        placeholder="Name"
      />
    </div>
  )
}

useEffect - Side Effects

import { useState, useEffect } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  
  // Fetch user data
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true)
      try {
        const response = await fetch(`/api/users/${userId}`)
        const userData = await response.json()
        setUser(userData)
      } catch (error) {
        console.error('Failed to fetch user:', error)
      } finally {
        setLoading(false)
      }
    }
    
    fetchUser()
  }, [userId]) // Dependency array
  
  // Cleanup function
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Polling...')
    }, 5000)
    
    return () => clearInterval(timer) // Cleanup
  }, [])
  
  if (loading) return <div>Loading...</div>
  
  return <div>{user?.name}</div>
}

Advanced Hooks

useReducer - Complex State Logic

import { useReducer } from 'react'

const initialState = {
  items: [],
  loading: false,
  error: null
}

function todoReducer(state, action) {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, loading: action.payload }
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false }
    case 'ADD_ITEM':
      return { 
        ...state, 
        items: [...state.items, action.payload],
        loading: false 
      }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      }
    case 'TOGGLE_ITEM':
      return {
        ...state,
        items: state.items.map(item => 
          item.id === action.payload 
            ? { ...item, completed: !item.completed }
            : item
        )
      }
    default:
      return state
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState)
  
  const addTodo = (text) => {
    dispatch({
      type: 'ADD_ITEM',
      payload: {
        id: Date.now(),
        text,
        completed: false
      }
    })
  }
  
  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_ITEM', payload: id })
  }
  
  return (
    <div>
      {state.loading && <div>Loading...</div>}
      {state.error && <div>Error: {state.error}</div>}
      {state.items.map(item => (
        <div key={item.id}>
          <span 
            style={{ 
              textDecoration: item.completed ? 'line-through' : 'none' 
            }}
          >
            {item.text}
          </span>
          <button onClick={() => toggleTodo(item.id)}>
            Toggle
          </button>
        </div>
      ))}
    </div>
  )
}

useContext - Global State

import { createContext, useContext, useReducer } from 'react'

// Create context
const AppContext = createContext()

// Provider component
function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState)
  
  const value = {
    ...state,
    dispatch,
    // Action creators
    login: (user) => dispatch({ type: 'LOGIN', payload: user }),
    logout: () => dispatch({ type: 'LOGOUT' }),
    setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme })
  }
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  )
}

// Custom hook
function useApp() {
  const context = useContext(AppContext)
  if (!context) {
    throw new Error('useApp must be used within AppProvider')
  }
  return context
}

// Usage in components
function Header() {
  const { user, logout, theme } = useApp()
  
  return (
    <header className={`header ${theme}`}>
      <span>Welcome, {user?.name}</span>
      <button onClick={logout}>Logout</button>
    </header>
  )
}

useMemo - Expensive Calculations

import { useMemo, useState } from 'react'

function ExpensiveComponent({ items, filter }) {
  const [sortBy, setSortBy] = useState('name')
  
  // Expensive filtering and sorting
  const processedItems = useMemo(() => {
    console.log('Processing items...') // Θα τρέξει μόνο όταν χρειάζεται
    
    return items
      .filter(item => item.name.includes(filter))
      .sort((a, b) => {
        if (sortBy === 'name') {
          return a.name.localeCompare(b.name)
        }
        return a.date - b.date
      })
  }, [items, filter, sortBy]) // Dependencies
  
  return (
    <div>
      <select onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">Sort by Name</option>
        <option value="date">Sort by Date</option>
      </select>
      
      {processedItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  )
}

useCallback - Function Memoization

import { useCallback, useState, memo } from 'react'

// Child component που θα re-render μόνο όταν χρειάζεται
const ExpensiveChild = memo(({ onItemClick, items }) => {
  console.log('ExpensiveChild rendered')
  
  return (
    <div>
      {items.map(item => (
        <button 
          key={item.id}
          onClick={() => onItemClick(item.id)}
        >
          {item.name}
        </button>
      ))}
    </div>
  )
})

function ParentComponent() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ])
  
  // Χωρίς useCallback, η function θα δημιουργείται πάντα από την αρχή
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId)
    // Κάποια logic εδώ
  }, []) // Αν δεν έχει dependencies, η function δεν θα αλλάξει ποτέ
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment (δεν επηρεάζει το child)
      </button>
      
      <ExpensiveChild 
        items={items}
        onItemClick={handleItemClick}
      />
    </div>
  )
}

Custom Hooks

useLocalStorage Hook

import { useState, useEffect } from 'react'

function useLocalStorage(key, initialValue) {
  // State για να κρατάμε την τρέχουσα τιμή
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error('Error reading from localStorage:', error)
      return initialValue
    }
  })
  
  // Function για να ενημερώνουμε το state και localStorage
  const setValue = (value) => {
    try {
      // Αν η value είναι function, την καλούμε με την προηγούμενη τιμή
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error('Error writing to localStorage:', error)
    }
  }
  
  return [storedValue, setValue]
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [language, setLanguage] = useLocalStorage('language', 'en')
  
  return (
    <div>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Current theme: {theme}
      </button>
      
      <select 
        value={language} 
        onChange={(e) => setLanguage(e.target.value)}
      >
        <option value="en">English</option>
        <option value="el">Greek</option>
      </select>
    </div>
  )
}

useFetch Hook

import { useState, useEffect, useCallback } from 'react'

function useFetch(url, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  const fetchData = useCallback(async () => {
    try {
      setLoading(true)
      setError(null)
      
      const response = await fetch(url, options)
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      const result = await response.json()
      setData(result)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }, [url, JSON.stringify(options)])
  
  useEffect(() => {
    fetchData()
  }, [fetchData])
  
  const refetch = () => {
    fetchData()
  }
  
  return { data, loading, error, refetch }
}

// Usage
function UserList() {
  const { data: users, loading, error, refetch } = useFetch('/api/users')
  
  if (loading) return <div>Loading users...</div>
  if (error) return <div>Error: {error}</div>
  
  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      {users?.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

useDebounce Hook

import { useState, useEffect } from 'react'

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)
    
    return () => {
      clearTimeout(handler)
    }
  }, [value, delay])
  
  return debouncedValue
}

// Usage για search
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 500)
  const [results, setResults] = useState([])
  
  useEffect(() => {
    if (debouncedSearchTerm) {
      // Κάνε search μόνο όταν ο χρήστης σταματήσει να γράφει
      searchAPI(debouncedSearchTerm).then(setResults)
    }
  }, [debouncedSearchTerm])
  
  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  )
}

Performance Patterns

Lazy Initialization

function ExpensiveComponent() {
  // ❌ Κακό - τρέχει κάθε render
  const [data, setData] = useState(expensiveCalculation())
  
  // ✅ Καλό - τρέχει μόνο μια φορά
  const [data, setData] = useState(() => expensiveCalculation())
  
  return <div>{data}</div>
}

Conditional Effects

function UserProfile({ userId, shouldPoll }) {
  const [user, setUser] = useState(null)
  
  // Effect που τρέχει μόνο όταν χρειάζεται
  useEffect(() => {
    if (!shouldPoll) return
    
    const interval = setInterval(async () => {
      const userData = await fetchUser(userId)
      setUser(userData)
    }, 5000)
    
    return () => clearInterval(interval)
  }, [userId, shouldPoll])
  
  return <div>{user?.name}</div>
}

Best Practices

1. Rules of Hooks

  • Πάντα καλέστε hooks στο top level
  • Μην τα καλείτε μέσα σε loops, conditions ή nested functions
  • Χρησιμοποιήστε το ESLint plugin: eslint-plugin-react-hooks

2. Dependency Arrays

// ❌ Κακό - missing dependency
useEffect(() => {
  fetchData(userId)
}, []) // Missing userId dependency

// ✅ Καλό
useEffect(() => {
  fetchData(userId)
}, [userId])

// ❌ Κακό - object στο dependency array
useEffect(() => {
  doSomething(config)
}, [config]) // Object reference αλλάζει κάθε render

// ✅ Καλό - destructure τα properties που χρειάζεστε
useEffect(() => {
  doSomething(config)
}, [config.apiUrl, config.timeout])

3. Custom Hook Naming

Πάντα ξεκινάτε με "use":

// ✅ Καλό
function useCounter() { ... }
function useApi() { ... }
function useFormValidation() { ... }

// ❌ Κακό
function counter() { ... }
function apiHook() { ... }

Συμπερασμάτα

Τα React Hooks είναι πολύ ισχυρά εργαλεία που μπορούν να κάνουν τον κώδικά σας πιο clean και reusable. Ξεκινήστε με τα βασικά hooks και προοδεύστε σταδιακά στα πιο προχωρημένα patterns.

Βασικές αρχές:

  • Ακολουθήστε τα Rules of Hooks
  • Χρησιμοποιήστε σωστά τα dependency arrays
  • Δημιουργήστε custom hooks για reusable logic
  • Προσέξτε την performance με memoization όπου χρειάζεται

Θέλετε να μάθετε περισσότερα για advanced React patterns; Επικοινωνήστε μαζί μας για εξειδικευμένα workshops!

Μοιραστείτε αυτό το άρθρο