אני מניחה שמפתחי ריאקט מכירים את המושג hook, ומשתמשים בו בעבודה באופן שוטף.
useState, useEffect לדוגמא, הם hooks שמובנים בReact. יש עוד מספר hooks מובנים. יש מה להרחיב עליהם אבל לא נתמקד בהם.

אני רוצה להדגים איך ליצור custom hook ולהשתמש בו.
מכיוון שצורת הלמידה שלי היא דרך האצבעות, לא אאריך בהקדמות אלא אגש ישר לעניין.

נניח שאני רוצה לכתוב קומפוננטה של שעון פשוט. הקומפוננטה מציגה מספרים בפורמט h:m:s כשכך אחד מהנתונים מתעדכן בשעת הצורך.
בצורה הנאיבית זו הקומפוננטה שלי:

קוד:
export const NaiveClock: React.FC = () => {

    const MAX_SECOND = 60;
    const MAX_MINUTE = 60;
    const MAX_HOUR = 24;

    const SECOND_INTERVAL = 1000;
    const MIUNTE_INTERVAL = SECOND_INTERVAL * MAX_SECOND;
    const HOUR_INTERVAL = MIUNTE_INTERVAL * MAX_MINUTE;

    const [seconds, setSeconds] = useState(0);
    const [minutes, setMinutes] = useState(0);
    const [hours, setHours] = useState(0);

    setTimeout(() => setSeconds(seconds < MAX_SECOND - 1 ? seconds + 1 : 0), SECOND_INTERVAL);

    setTimeout(() => setMinutes(minutes < MAX_MINUTE - 1 ? minutes + 1 : 0), MIUNTE_INTERVAL);

    setTimeout(() => setHours(hours < MAX_HOUR - 1 ? hours + 1 : 0), HOUR_INTERVAL);

    return <>
        <div>Good Morning Naive Clock</div>
        <div>{hours}:{minutes}:{seconds}</div>
    </>;
}

זה עובד, אבל לא נראה טוב.
יש כאן 3 שורות שזועקות לעזרה:

קוד:
    setTimeout(() => setSeconds(seconds < MAX_SECOND - 1 ? seconds + 1 : 0), SECOND_INTERVAL);
    setTimeout(() => setMinutes(minutes < MAX_MINUTE - 1 ? minutes + 1 : 0), MIUNTE_INTERVAL);
    setTimeout(() => setHours(hours < MAX_HOUR - 1 ? hours + 1 : 0), HOUR_INTERVAL);

הלוגיקה שלהן ממש זהה, ומדגדג לי לכתוב אותן באיזשהו אופן שיאפשר reuse.
אז בשלב הראשון אני כותבת פונקציה כזאת:

קוד:
const updateTime = (time: number, max: number): number => {
    return time < max - 1 ? time + 1 : 0;
}
ומשתמשת בה לכל אחד מהנתונים: שעות, דקות ושניות.

עכשיו הקומפוננטה נראית כך:
קוד:
const updateTime = (time: number, max: number): number => {
    return time < max - 1 ? time + 1 : 0;
}

export const NaiveClock: React.FC = () => {

    const MAX_SECOND = 60;
    const MAX_MINUTE = 60;
    const MAX_HOUR = 24;

    const SECOND_INTERVAL = 1000;
    const MIUNTE_INTERVAL = SECOND_INTERVAL * MAX_SECOND;
    const HOUR_INTERVAL = MIUNTE_INTERVAL * MAX_MINUTE;

    const [seconds, setSeconds] = useState(0);
    const [minutes, setMinutes] = useState(0);
    const [hours, setHours] = useState(0);

    setTimeout(() => setSeconds(updateTime(seconds, MAX_SECOND)), SECOND_INTERVAL);

    setTimeout(() => setMinutes(updateTime(minutes, MAX_MINUTE)), MIUNTE_INTERVAL);

    setTimeout(() => setHours(updateTime(hours, MAX_HOUR)), HOUR_INTERVAL);

    return <>
        <div>Good Morning Naive Clock</div>
        <div>{hours}:{minutes}:{seconds}</div>
    </>;
}

יותר טוב, אבל עדיין לא מושלם.
איך אפשר לחסוך בsetTimout? גם הוא חוזר על עצמו אבל לכאורה אין מה לעשות איתו. העדכון מתבצע בכל פעם על משתנה אחר, ולשלוח את הsetSeconds/setMinutes/setHours בתור callback זה אהממ... לא כל כך נראה לי הפתרון.
זה עלול לגרום למפתחים הבאים שעובדים על המערכת לפספס את המקום שבו הstate משתנה.

ובכן, הפתרון הוא custom hook.
אכתוב hook שמשמש להחזרת ערך מעודכן מידי פרק זמן שיתקבל כפרמטר, ויאפס אותו כשהערך שלו מגיע למקסימום, שגם הוא כמובן מתקבל כפרמטר.
קוד:
const useIncrease = (interval: number, max: number) => {
    const [number, setNumber] = useState(0);

    const increase = () => {
        setNumber(number < max - 1 ? number + 1 : 0);
    }

    setTimeout(increase, interval * 1000);
    return number;
}
במבט ראשון מדובר בפונקציה רגילה.
מה הופך אותה לhook? מבחינת הקומפיילר, כל פונקציה שהשם שלה כפוף לקונבנציה useSomething, היא hook.
דבר נוסף ומעניין הוא, שהhook משתמש בuseState, מה שמאפשר לו לשמור ערכים לאורך כל החיים שלו. (הערכים כמובן נשמרים בנפרד לכל מופע של הhook).
יתכן שההגדרה הנכונה לגבי hook, היא: קומפוננטה ללא UI. קומפוננטה לוגית ולא ויזואלית.

כעת אני יכולה להחליף את הקריאות הכפולות והמסורבלות לsetTimeout, בקריאה לhook:
קוד:
    const second = useIncrease(SECOND_INTERVAL, MAX_SECOND);
    const minute = useIncrease(MIUNTE_INTERVAL, MAX_MINUTE);
    const hour = useIncrease(HOUR_INTERVAL, MAX_HOUR);

והקומפוננטה במלואה נראית כך:
קוד:
export const Clock: React.FC = () => {

    const MAX_SECOND = 60;
    const MAX_MINUTE = 60;
    const MAX_HOUR = 24;

    const SECOND_INTERVAL = 1000;
    const MIUNTE_INTERVAL = SECOND_INTERVAL * MAX_SECOND;
    const HOUR_INTERVAL = MIUNTE_INTERVAL * MAX_MINUTE;

    const second = useIncrease(SECOND_INTERVAL, MAX_SECOND);
    const minute = useIncrease(MIUNTE_INTERVAL, MAX_MINUTE);
    const hour = useIncrease(HOUR_INTERVAL, MAX_HOUR);

    return <>
        <div>Good Morning Smart Clock</div>
        <div>{hours}:{minutes}:{seconds}</div>
    </>;
}

זה הכל!
התוצאה תהיה זהה לקומפוננטה הנאיבית.
יפה, נכון?

כמה הערות לגבי hooks:
1. כפי שכבר הזכרתי, הקונבנציה היא לקרוא לhook בשם במבנה useSomething.
2. ניתן להשתמש בhook רק מתוך FC (Functional Component) או מתוך hook אחר.
3. בשום אופן אין לבצע קריאה לhook בתוך if, בתוך לולאה, או בתוך פונקציה רגילה. (הסיבה היא שreact משתמש בסדר של הקריאות לhooks לצורך חישובים, וברגע שמספר הקריאות לא יציב, או שקריאה אחת נקראת רק במקרים מסוימים, הסדר משתבש וזה עשוי לגרום לבאגים חמורים.)

נ. ב.
1. לא טיפלתי בשרשור של אפסים כשהערכים קטנים מ-10.
2. אפשר לשכלל את הhook יותר כך שיקבל גם initial data, ולשלוח את הזמן הנוכחי. מוזמנים לנסות בעצמכם.
3. מסיבה מסתורית כלשהי (מניחה שקשור לצורה שsetTimeout עובד), כל עדכון לוקח כמה אלפיות השניה, כך שלאורך זמן הסינכרון לא מדויק.

nathan-dumlao-5Hl5reICevY-unsplash.jpg

לא עדיף כבר להשתמש בשעון חול? :unsure:
עריכה: רותי העירה לי שאפשר פשוט לשמור רק את מספר השניות, ומידי שינוי לחשב את הדקות ע"י חלוקה ב-60, ואת השעות ע"י חלוקה ב-60*60.
אז כמובן, זה יעבוד הרבה יותר טוב והסינכרון יהיה מדויק.
אבל אני משאירה את זה ככה, כי המטרה שלי היא לא ליצור שעון חכם בreact, אלא להדגים את הצורך והשימוש בcustom hook.