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!