עיון ב-Hooks API
Hooks הם תוספת חדשה ב-React 16.8. הם נותנים לנו להשתמש ב-state ופיצ’רים אחרים של React מבלי לכתוב מחלקה.
עמוד זה מתאר את ה-APIs של ה-Hooks המובנים בתוך React.
אם הנושא של Hooks חדש לך, יכול להיות שתרצה לקרוא את הסקירה הכללית קודם. יכול להיות שתמצא מידע שימושי בסעיף שאלות נפוצות.
Hooks בסיסיים
useState
const [state, setState] = useState(initialState);
מחזיר ערך stateful, ופונקציה על מנת לעדכן אותו.
בזמן הרינדור הראשוני, ה-state המוחזר (state
) הוא שווה ערך לערך המועבר כארגומנט הראשון (initialState
).
פונקציית ה-setState
משמשת לעדכון ה-state. היא מקבלת ערך state חדש וקובעת רינדור מחדש של הקומפוננטה.
setState(newState);
בזמן הרינדורים העוקבים, הערך הראשון שמוחזר על ידי useState
תמיד יהיה ה-state האחרון לאחר יישום העדכונים.
הערה
React מבטיח שזהות פונקציית ה-
setState
יציבה ולא תשתנה בין רינדורים. זאת הסיבה שזה בטוח להשמיט
עדכונים פונקציונליים
אם ה-state החדש חושב באמצעות ה-state הקודם, ניתן להעביר פונקציה ל-setState
. הפונקציה תקבל את הערך הקודם, ותחזיר ערך מעודכן. הנה דוגמה של קומפוננטת counter שמשתמשת בשתי הצורות של setState
:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
כפתורי ה-”+” וה-”-” משתמשים בצורה הפונקציונלית, בגלל שהערך המעודכן מבוסס על הערך הקודם. אבל כפתור ה”Reset” משתמש בצורה הרגילה, בגלל שהוא תמיד מעדכן את הספירה חזרה לערך ההתחלתי.
הערה אם הפונקציה מחזירה ערך שווה לזה שקיים ב-state הנוכחי, הרינדור הבא ידולג לגמרי.
בשונה ממתודת ה-
setState
שנמצאת בקומפוננטות מחלקה,useState
לא ממזגת עדכוני אובייקטים באופן אוטומטי. ניתן לחקות התנהגות זו על ידי שילוב של מעדכן פונקציה עם אופן הכתיבה של object spread(שלוש נקודות ’…’):setState(prevState => { // Object.assign גם יעבוד return {...prevState, ...updatedValues}; });
אופציה נוספת היא
useReducer
, שמתאים יותר לניהול אובייקטי(objects) state שמכילים מספר רב של תת-ערכים.
Initial state עצלן
הארגומנט initialState
הוא ה-state שהשתמשנו בו ברינדור הראשון. ברינדורים עוקבים, מתעלמים ממנו. אם ה-state ההתחלתי הוא התוצאה של חישוב יקר, ניתן לספק פונקציה במקום, שתרוץ רק ברינדור ההתחלתי:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
יציאה מעדכון state
אם אתה מעדכן State Hook לערך ששווה לערך הנוכחי, React יצא מהפעולה מבלי רינדור הילדים או יריית אפקטים. (React משתמש באלגוריתם ההשוואה Object.is
.)
שים לב שיכול להיות ש-React יצטרך לרנדר את הקומפוננטה הספציפית הזו לפני יציאה מהפעולה. זה לא אמור להיות מדאיג בגלל ש-React לא ילך שלא כצורך “עמוק” לתוך העץ. אם אתה מבצע חישובים יקרים בזמן רינדור, ניתן למטב אותם עם useMemo
.
useEffect
useEffect(didUpdate);
מקבלת פונקציה שמכילה קוד חיוני, שכנראה גורם לאפקט כלשהו.
Mutations, subscriptions, טיימרים, לוגים, ותופעות לוואי אחרים לא מורשים בתוך ה-main body של קומפוננטת פונקציה (המכונה שלב הרינדור של React). אי ציות לכך יגרום לבאגים מבלבלים ואי עקביות בממשק המשתמש.
במקום זאת, השתמש ב-useEffect
. הפונקציה המועברת ל-useEffect
תרוץ אחרי שהרינדור מופיע על המסך. ניתן לחשוב על אפקטים כפתח מילוט מהעולם הפונקציונלי של React לתוך העולם האימפרטיבי.
כברירת מחדל, אפקטים רצים אחרי כל רינדור שמסתיים, אבל ניתן לבחור להריץ אותם רק כשערכים מסוימים שונו.
ניקוי אפקט
לעיתים קרובות, אפקטים יוצרים משאבים שדורשים ניקוי לפני שהקומפוננטה עוזבת את המסך, כמו subscription או timer ID. על מנת לעשות זאת, הפונקציה המועברת ל-useEffect
תחזיר פונקציית נקיון. לדוגמה, על מנת ליצור subscription:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// ניקוי ה-subscription
subscription.unsubscribe();
};
});
פונקציית הנקיון רצה לפני הסרת הקומפוננטה מממשק המשתמש על מנת למנוע דליפות זיכרון. בנוסף לכך, אם קומפוננטה מתרנדרת מספר רב של פעמים (כמו שבדרך כלל קורה), האפקט הקודם מנוקה לפני הרצת האפקט הבא. בדוגמה שלנו, זה אומר ש-subscription חדש נוצר בכל עדכון. על מנת להימנע מיריית אפקט על כל עדכון, קרא את החלק הבא.
תזמון אפקטים
בשונה מ-componentDidMount
ו-componentDidUpdate
, הפונקציה שמועברת ל-useEffect
נורה לאחר פריסה וצביעה(layout and paint), בזמן אירוע נדחה. זה עושה את זה מתאים להרבה תופעות לוואי, כמו הכנת subscriptions ו- event handlers, בגלל שרוב סוגי העבודה לא חוסמים את הדפדפן מלעדכן את המסך.
למרות זאת, לא ניתן לעכב את כל האפקטים. לדוגמה, מוטציית DOM שגלויה למשתמש צריכה להיות נורה באופן סינכרוני לפני הצבע הבא כך שהמשתמש לא יבחין בחוסר עקביות חזותי. (ההבחנה דומה מבחינה קונספטואלית למאזינים לאירועים פסיביים לעומת פעילים). בשביל סוגי האפקטים האלה, React מספק Hook נוסף שנקרא useLayoutEffect
. יש לו את אותה חתימה כ-useEffect
, ושונה ממנו כשהוא נורה.
אף על פי ש-useEffect
מתעכב על שהדפדפן נצבע, זה מובטח שהוא נורה לפני רינדורים חדשים. React תמיד ינקה אפקטים של רינדורים קודמים לפני החלת עדכון חדש.
יריית אפקט לפי תנאי
ההתנהגות הרגילה של אפקטים היא לירות את האפקט לאחר כל רינדור שהושלם. בדרך זו אפקט תמיד נוצר מחדש אם אחד מה-dependencies שלו משתנה.
למרות זאת, זה יכול להיות יותר מדי במקרים מסוימים, כמו דוגמת ה-subscription מהקטע הקודם. אנחנו לא צריכים ליצור subscription חדש על כל עדכון, רק אם ה-prop source
שונה.
על מנת ליישם זאת, העבר ארגומנט שני ל-useEffect
שהוא מערך של ערכים שהאפקט תלוי בהם. הדוגמה המעודכנת שלנו נראית כמו זה:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
עכשיו ה-subscription ייווצר מחדש רק כש-props.source
משתנה.
הערה
אם אתה משתמש באופטימיזציה זו, וודא כי המערך מכיל את כל הערכים מ-scope הקומפוננטה (כמו props ו-state) שמשתנים לאורך זמן ושהאפקט משתמש בהם. אחרת, הקוד שלך יתייחס לערכים ישנים מרינדורים קודמים. למד עוד על על איך לטפל בפונקציות ומה לעשות כשערכי המערך משתנים בתדירות גבוהה מדי.
אם אתה רוצה להריץ אפקט ולנקות אותו רק פעם אחת (ב-mount ו-unmount), תוכל להעביר מערך ריק ( [] ) כארגומנט שני. זה אומר ל-React שהאפקט שלך לא תלוי בשום ערך מה-props או state, כך שהוא לא צריך לרוץ מחדש. זה לא מטופל כמקרה מיוחד – זה עובד כמו שמערך ה-dependencies תמיד עובד.
אם אתה מעביר מערך ריק ( [] ), ה-props ו-state בתוך האפקט תמיד יכילו את הערכים ההתחלתיים שלהם. בזמן שהעברת
[]
כארגומנט שני יותר קרוב ל-componentDidMount
ו-componentWillUnmount
כמודל מנטלי, יש פתרונות טובים יותר שעוזרים להימנע מהרצה מחדש של אפקטים בתדירות גבוהה מדי. בנוסף, אסור לשכוח ש-React מעכב הרצה שלuseEffect
עד לאחר שהדפדפן נצבע, אז עשיית עבודה נוספת היא פחות בעיה.אנו ממליצים על שימוש בחוק
exhaustive-deps
כחלק מחבילת ה-eslint-plugin-react-hooks
שלנו. הוא מזהיר מפני dependencies שמוגדרים לא נכון ומציע תיקון.
מערך ה-dependencies לא מועבר כארגומנטים לפונקציית האפקט. אבל באופן עקרוני, זה מה שהם מייצגים: כל ערך שמצוין בתוך פונקציית האפקט צריך להופיע במערך ה-dependencies. בעתיד, קומפיילר מתקדם יוכל ליצור את המערך באופן אוטומטי.
useContext
const value = useContext(MyContext);
מקבל אובייקט context (הערך המוחזר מ-React.createContext
) ומחזיר את ערך ה-context הנוכחי לאותו context. ערך ה-context הנוכחי נקבע על ידי ה-prop value
של <MyContext.Provider
מעל הקומפוננטה הקוראת בעץ.
כש-<MyContext.Provider>
מעל הקומפוננטה מתעדכן, ה-Hook מפעיל מרנדר עם value
האחרון של ה-context, ואותו ערך מועבר ל- MyContext
Provider.
אל תשכח שהארגומנט של useContext
צריך להיות אובייקט ה-context עצמו:
נכון: useContext(MyContext)
לא נכון: useContext(MyContext.Consumer)
לא נכון: useContext(MyContext.Provider)
קומפוננטה שקוראת ל-useContext
תמיד תתרנדר מחדש כשערך ה-context ישתנה. אם רינדור מחדש של הקומפוננטה הוא יקר, ניתן למטב אותו על ידי שימוש ב-memoization.
טיפ
אם אתה מכיר את ה-context API לפני Hooks,
useContext(MyContext)
הוא שווה ל-static contextType = MyContext
במחלקה, או ל-<MyContext.Consumer>
.
useContext(MyContext)
נותן לנו רק לקרוא את ה-context ולעשות subscribe לשינויים שלו. נצטרך עדיין<MyContext.Provider>
מעל בעץ על מנת לספק את הערך ל-context זה.
Hooks נוספים
ה-Hooks הבאים הם או צורות אחרות של הבסיסיים מהסעיף הקודם, או כאלה שנצטרך רק במקרי קצה ספציפיים. לא צריך להילחץ מללמוד אותם בהתחלה.
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
אלטרנטיבה ל-useState
. מקבל reducer מסוג (state, action) => newState
, ומחזיר את ה-state הנוכחי ביחד עם מתודת dispatch
. (אם התעסקת בעבר עם Redux, זה כבר מוכר לך).
בדרך כלל useReducer
עדיף על useState
כשיש לך לוגיקת state מורכבת שמערבת מספר רב של תת-ערכים או כשה-state הבה תלוי ב-state הקודם. useReducer
גם נותן לנו למטב ביצועים לקומפוננטות שמפעילות עדכונים עמוקים בגלל שניתן להעביר את dispatch מטה במקום callbacks.
הנה דוגמת ה-counter מהקטע הקודם על useState
, נכתב מחדש עם שימוש ב-reducer:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
הערה
React מבטיח שזהות פונקציית
dispatch
היא קבועה ולא תשתנה ברינדורים חוזרים. זאת הסיבה שזה בטוח להשמיט מרשימת ה-dependency שלuseEffect
אוuseCallback
.
ציון ה-state ההתחלתי
ישנם שתי דרכים שונות לאתחל useReducer
state. ניתן לבחור אחד מהם תלוי בשימוש. הדרך הפשוטה ביותר היא להעביר את ה-state ההתחלתי כארגומנט שני:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount} );
הערה
React לא משתמש במוסכמת הארגומנט
state = intialState
בניגוד ל-Redux. הערך ההתחלתי לפעמים תלוי ב-props ומכאן שהוא מוגדר בקריאת ה-Hook. אם זה לא לטעמך, ניתן לקרוא ל-useReducer(reducer, undefined, reducer)
על מנת לחקות את התנהגות Redux, אבל זה לא מומלץ.
אתחול עצלן
ניתן גם ליצור state התחלתי בעצלתיים. על מנת לעשות זאת, ניתן להעביר פונקציית init
כארגומנט שלישי. ה-state ההתחלתי ייקבע ל-init(initialArg)
.
זה נותן לנו לחלץ את הלוגיקה לחישוב ה-state ההתחלתי מחוץ ל-reducer. זה גם שימושי לאיפוס ה-state לאחר מכן כתגובה לפעולה(action):
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset': return init(action.payload); default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init); return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
יציאה מ-dispatch
אם אתה מעדכן State Hook לערך ששווה לערך הנוכחי, React יצא מהפעולה מבלי רינדור הילדים או יריית אפקטים. (React משתמש באלגוריתם ההשוואה Object.is
.)
שים לב שיכול להיות ש-React יצטרך לרנדר את הקומפוננטה הספציפית הזו לפני יציאה מהפעולה. זה לא אמור להיות מדאיג בגלל ש-React לא ילך שלא כצורך “עמוק” לתוך העץ. אם אתה מבצע חישובים יקרים בזמן רינדור, ניתן למטב אותם עם useMemo
.
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
מחזיר memoized callback.
העבר callback ומערך של dependencies. useCallback
תחזיר גרסה memorized של ה-callback שמשתנה רק אם אחד מה-dependencies משתנה. זה שימושי כשמעבירים callbacks לקומפוננטות ילדים ממוטבות שמסתמכות על השוואה לפי אזכור על מנת למנוע רינדורים מיותרים (לדוגמה shouldComponentUpdate
).
useCallback(fn, deps)
שווה ל-useMemo(() => fn, deps)
.
הערה
מערך ה-dependencies לא מועבר כארגומנטים לפונקציית האפקט. אבל באופן עקרוני, זה מה שהם מייצגים: כל ערך שמצוין בתוך פונקציית האפקט צריך להופיע במערך ה-dependencies. בעתיד, קומפיילר מתקדם יוכל ליצור את המערך באופן אוטומטי.
אנו ממליצים על שימוש בחוק
exhaustive-deps
כחלק מחבילת ה-eslint-plugin-react-hooks
שלנו. הוא מזהיר מפני dependencies שמוגדרים לא נכון ומציע תיקון.
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
מחזיר ערך memoized.
העבר פונקציית “create” ומערך של dependencies. useMemo
תחשב מחדש רק את הערך ה-memoized כשאחד מה-dependencies שונה. מיטוב זה עוזר להימנע מחישובים יקרים בכל רינדור.
זכור כי הפונקציה שמועברת ל-useMemo
רצה בזמן רינדור. אל תעשה דברים בתוכה שלא היית עושה בדרך כלל בזמן רינדור. לדוגמה, side effects שייכים ל-useEffect
, לא useMemo
.
אם סופק מערך כלשהו, ערך חדש יחושב בכל רינדור.
ניתן להסתמך על useMemo
כמיטוב ביצועים, לא כערבות סמנטית. בעתיד, יכול להיות ש-React יבחר “לשכוח” חלק מהערכים ה-memoized ויחשב אותם מחדש ברינדור הבא, לדוגמה, על מנת לשחרר זיכרון לקומפוננטות offscreen. כתוב את הקוד שלך כך שהוא יעבוד בלי useMemo
— ואז תוסיף אותו על מנת למטב ביצועים.
הערה
מערך ה-dependencies לא מועבר כארגומנטים לפונקציית האפקט. אבל באופן עקרוני, זה מה שהם מייצגים: כל ערך שמצוין בתוך פונקציית האפקט צריך להופיע במערך ה-dependencies. בעתיד, קומפיילר מתקדם יוכל ליצור את המערך באופן אוטומטי.
אנו ממליצים על שימוש בחוק
exhaustive-deps
כחלק מחבילת ה-eslint-plugin-react-hooks
שלנו. הוא מזהיר מפני dependencies שמוגדרים לא נכון ומציע תיקון.
useRef
const refContainer = useRef(initialValue);
useRef
מחזיר אובייקט ref שניתן לשינוי שמאפיין ה-.current
שלו מאותחל לארגומנט המועבר (intialValue
). האובייקט המוחזר יתמיד לכל מחזור החיים של הקומפוננטה.
מקרה שימוש נפוץ הוא לגשת לילד כשרוצים:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` מצביע על אלמנט ה-text input
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
במהותו, useRef
הוא כמו “קופסה” שיכולה להחזיק ערך שניתן לשינוי בתוך מאפיין ה-.current
.
אולי אתה מכיר refs בעיקר כדרך לגשת ל-DOM. אם אתה מעביר אובייקט ref ל-React עם <div ref={myRef}
, React יקבע את מאפיין ה-.current
ל-DOM node המקביל כשאותו node משתנה.
למרות זאת, useRef()
שימושי ליותר מתכונת ה-ref
. הוא שימושי לשמירת כל ערך שניתן לשינוי בדומה לדרך שהיית משתמש ב-instance fields במחלקות.
זה עובד בגלל שuseRef()
יוצר אובייקט JavaScript פשוט. ההבדל היחיד בין useRef()
ויצירת אובייקט {current: …}
בעצמך היא ש-useRef()
ייתן לך את אותו אובייקט ref בכל רינדור.
זכור ש-useRef()
לא מודיע לך כשהתוכן שלו משתנה. שינוי של המאפיין .current
לא גורם לרינדור מחדש. אם אתה רוצה להריץ קוד כש-React מצרף או מנתק ref מ-DOM node, אולי תרצה להשתמש ב-callback ref במקום.
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle
מתאים אישית את הערך ה-instance שנחשף לקומפוננטות הורה בשימוש ref
. כמו תמיד, כדאי להימנע מקוד אימפרטיבי בשימוש refs ברוב המקרים. כדאי להשתמש ב-useImperativeHandle
עם forwardRef
:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
בדוגמה זו, קומפוננטת הורה שמרנדרת <FancyInput ref={fancyInputRef} />
צריכה להיות מסוגלת לקרוא ל-fancyInputRef.current.focus()
.
useLayoutEffect
מאפיין זה זהה ל-useEffect
, אבל הוא יורה באופן סינכרוני לאחר כל שינויי DOM. השתמש בזה על מנת לקרוא layout מתוך ה-DOM ולרנדר מחדש באופן סינכרוני. עדכונים מתוזמנים בתוך useLayoutEffect
ישטפו באופן סינכרוני, לפני שלדפדפן יש הזדמנות לצבוע.
העדף את useEffect
הסטנדרטי מתי שאפשר על מנת להימנע מחסימת עדכונים ויזואליים.
טיפ
אם אתה מזיז קוד מקומפוננטת מחלקה, שים לב ש-
useLayoutEffect
יורה באותו קצב כמוcomponentDidMount
ו-componentDidUpdate
. לעומת זאת, אנו ממליצים להתחיל עםuseEffect
קודם ולנסות אתuseLayoutEffect
רק אם זה יוצר בעיה.אם אתה משתמש ב-server rendering, שים לב שגם
useLayoutEffect
וגםuseEffect
יכולים לרוץ עד שה-Javascript הורד. זאת הסיבה ש-React מזהיר כשקומפוננטה שהיא server-rendered מכילהuseLayoutEffect
. על מנת לתקן זאת, או שתעביר את הלוגיקה ל-useEffect
(אם זה לא נחוץ לרינדור הראשון), או המתן עם הצגת הקומפוננטה עד לאחר רינדור הקליינט (אם ה-HTML נראה שבור עד ש-useLayoutEffect
רץ).על מנת להדיר קומפוננטה שצריכה layout effects מ-server-rendered HTML, רנדר אותה בתנאי עם
showChild && <Child />
ועכב את הצגתה עםuseEffect(() => { setShowChild(true); }, [])
. בדרך זו, ממשק המשתמש לא מופיע שבור לפני הידרציה.
useDebugValue
useDebugValue(value)
ניתן להשתמש ב-useDebugValue
על מנת להציג label ל-hooks מותאמים אישית ב-React DevTools.
לדוגמה, שקול את ה-hook useFriendStatus
שמתואר ב”בניית Hooks משלך”:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// הראה label ב-DevTools ליד ה-Hook הזה // לדוגמה "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
טיפ
אנו לא ממליצים להוסיף ערכי debug לכל Hook מותאם אישית. זה נחוץ במיוחד ל-Hooks מותאמים אישית שחלק מספריות משותפות.
דחה formatting של ערכי debug
במקרים מסוימים לבצע formatting לערך יכול להיות פעולה יקרה. זה גם לא נחוץ אלא אם ה-Hook נבדק.
מסיבה זו useDebugValue
מקבל פונקציית formatting כפרמטר שני אופציונלי. קוראים לפונקציה זו רק אם ה-Hooks נבדקים. היא מקבלת את ערך ה-debug כפרמטר וצריכה להחזיר ערך הצגה שעבר formatting.
לדוגמה Hook מותאם אישית שמחזיר ערך Date
יוכל להימנע מלקרוא לפונקציית toDateString
באופן לא נחוץ על ידי העברת ה-formatter הבא:
useDebugValue(date, date => date.toDateString());