Custom Hooks
מבוא
Custom Hook הוא פונקציה ששמה מתחיל ב-use ומשתמשת בתוכה ב-Hooks אחרים. תפקידו לחלץ לוגיקה החוזרת על עצמה - כמו שליפת נתונים מהשרת או האזנה לגודל החלון - למקום אחד, כדי שאפשר יהיה לעשות בה שימוש חוזר במספר קומפוננטות בלי לשכפל קוד. זהו אחד הכלים העוצמתיים ביותר לשמירה על קוד נקי ומאורגן ב-React.
// במקום לשכפל קוד...
function Component1() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* fetch logic */ }, []);
}
function Component2() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* same fetch logic */ }, []);
}
// ...יוצרים Custom Hook!
function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* fetch logic */ }, [url]);
return { loading, data };
}
function Component1() {
const { loading, data } = useFetch('/api/data1');
}
כללים ל-Custom Hooks
- שם מתחיל ב-
use- חובה! (useMyHook, useFetch, useAuth) - קורא ל-Hooks אחרים - useState, useEffect, וכו'
- מחזיר ערכים - מה שהקומפוננטה צריכה
- לוגיקה טהורה - ללא JSX (זה לא קומפוננטה!)
דוגמאות Custom Hooks
1. useFetch - קריאות API
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// שימוש
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>טוען...</p>;
if (error) return <p>שגיאה: {error}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
2. useLocalStorage - שמירה מקומית
function useLocalStorage(key, initialValue) {
// קריאה מ-localStorage בטעינה
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
// שמירה ל-localStorage בכל שינוי
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, value]);
return [value, setValue];
}
// שימוש
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">בהיר</option>
<option value="dark">כהה</option>
</select>
<input
type="range"
value={fontSize}
onChange={e => setFontSize(Number(e.target.value))}
min="12" max="24"
/>
</div>
);
}
3. useToggle - החלפת מצב
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// שימוש
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<>
<button onClick={toggle}>פתח מודל</button>
{isOpen && (
<div className="modal">
<p>תוכן המודל</p>
<button onClick={close}>סגור</button>
</div>
)}
</>
);
}
4. useDebounce - השהייה
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// שימוש - חיפוש עם השהייה
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
const { data: results } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="חפש..."
/>
{results && <SearchResults results={results} />}
</div>
);
}
5. useWindowSize - גודל חלון
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// שימוש
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? <MobileLayout /> : <DesktopLayout />}
</div>
);
}
6. useClickOutside - לחיצה מחוץ לאלמנט
function useClickOutside(ref, callback) {
useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [ref, callback]);
}
// שימוש
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>תפריט</button>
{isOpen && (
<ul className="dropdown-menu">
<li>אפשרות 1</li>
<li>אפשרות 2</li>
</ul>
)}
</div>
);
}
7. usePrevious - ערך קודם
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// שימוש
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>נוכחי: {count}</p>
<p>קודם: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
8. useForm - ניהול טפסים
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name) => (e) => {
const value = e.target.type === 'checkbox'
? e.target.checked
: e.target.value;
setValues(prev => ({ ...prev, [name]: value }));
};
const handleBlur = (name) => () => {
setTouched(prev => ({ ...prev, [name]: true }));
if (validate) {
const fieldError = validate({ [name]: values[name] });
setErrors(prev => ({ ...prev, ...fieldError }));
}
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
if (validate) {
const allErrors = validate(values);
setErrors(allErrors);
if (Object.keys(allErrors).length === 0) {
onSubmit(values);
}
} else {
onSubmit(values);
}
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// שימוש
function LoginForm() {
const validate = (values) => {
const errors = {};
if (!values.email?.includes('@')) errors.email = 'אימייל לא תקין';
if (values.password?.length < 6) errors.password = 'לפחות 6 תווים';
return errors;
};
const { values, errors, handleChange, handleBlur, handleSubmit } = useForm(
{ email: '', password: '' },
validate
);
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
value={values.email}
onChange={handleChange('email')}
onBlur={handleBlur('email')}
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={values.password}
onChange={handleChange('password')}
onBlur={handleBlur('password')}
/>
{errors.password && <span>{errors.password}</span>}
<button type="submit">התחבר</button>
</form>
);
}
סיכום
| Hook | שימוש |
|---|---|
useFetch |
קריאות API |
useLocalStorage |
שמירה מקומית |
useToggle |
מצב בוליאני |
useDebounce |
השהיית ערך |
useWindowSize |
גודל חלון |
useClickOutside |
לחיצה מחוץ |
usePrevious |
ערך קודם |
useForm |
ניהול טפסים |
הקודם: ← Context API
הבא: React Patterns →
לחזרה ותרגול חי: הנושא הזה מופיע גם
במדריך האינטראקטיבי ל-React, עם עורך קוד שמראה את התוצאה מיד.
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה חייב להיות בשם של Custom Hook?
הצגת התשובה
תשובה ב. הקידומת מאפשרת ל-React ולכלי lint לאכוף את חוקי ה-Hooks.
-
האם שתי קומפוננטות שמשתמשות באותו Custom Hook חולקות state?
הצגת התשובה
תשובה ד. Hook משתף לוגיקה, לא נתונים. לשיתוף נתונים משתמשים ב-Context.