שימוש חוזר בלוגיקה עם ווים מותאמים

React מגיע עם כמה Hooks מובנים כמו useState, useContext ו-useEffect. לפעמים, תרצה שיהיה Hook למטרה ספציפית יותר: למשל, להביא נתונים, לעקוב אם המשתמש מחובר או להתחבר לחדר צ’אט. אולי לא תמצא את ה-Hooks ב-React, אבל אתה יכול ליצור Hooks משלך לצרכי היישום שלך.

You will learn

  • מה הם הHooks מותאמים אישית ואיך לכתוב בעצמך
  • כיצד לעשות שימוש חוזר בלוגיקה בין רכיבים
  • איך לתת שם ולבנות את הווים המותאמים אישית שלך
  • מתי ומדוע לחלץ ווים מותאמים אישית

ווים מותאמים אישית: שיתוף היגיון בין רכיבים

תארו לעצמכם שאתם מפתחים אפליקציה שנשענת במידה רבה על הרשת (כמו שרוב האפליקציות עושות). אתה רוצה להזהיר את המשתמש אם חיבור הרשת שלו כבה בטעות בזמן שהוא השתמש באפליקציה שלך. איך היית מתנהלת? נראה שתצטרך שני דברים ברכיב שלך:

  1. פיסת state שעוקבת אחר האם הרשת מקוונת.
  2. אפקט שנרשם לאירועים הגלובליים מקוון ו-offline.

זה ישמור את הרכיב שלך מסונכרן עם סטטוס הרשת. אתה יכול להתחיל עם משהו כזה:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

נסה להפעיל ולכבות את הרשת שלך, ושם לב כיצד ‘סרגל הstate’ הזה מתעדכן בתגובה לפעולות שלך.

עכשיו דמיינו שאתם גם רוצים להשתמש באותו היגיון ברכיב אחר. ברצונך ליישם כפתור שמירה שיהפוך לבלתי זמין ויראה “מתחבר מחדש…” במקום “שמור” בזמן שהרשת כבויה.

כדי להתחיל, אתה יכול להעתיק ולהדביק את מצב isOnline ואת האפקט לתוך SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

ודא שאם תכבה את הרשת, הכפתור ישנה את מראהו.

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

חילוץ Hook מותאם אישית משלך מרכיב

תארו לעצמכם לרגע שבדומה ל-useState ו-useEffect, היה Hook של useOnlineStatus מובנה. אז ניתן יהיה לפשט את שני הרכיבים הללו ולהסיר את הכפילות ביניהם:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progress saved');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}

למרות שאין Hook מובנה כזה, אתה יכול לכתוב את זה בעצמך. הכריז על פונקציה בשם useOnlineStatus והעבר לתוכה את כל הקוד המשוכפל מהרכיבים שכתבת קודם:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

בסוף הפונקציה, החזר isOnline. זה מאפשר לרכיבים שלך לקרוא את הערך הזה:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

ודא כי הפעלה וכיבוי של הרשת מעדכנת את שני הרכיבים.

כעת לרכיבים שלך אין כל כך הרבה היגיון חוזר. חשוב מכך, הקוד בתוכם מתאר מה הם רוצים לעשות (השתמשו בסטטוס המקוון!) במקום איך לעשות זאת (על ידי הרשמה לאירועי הדפדפן).

כאשר אתה מחלץ היגיון לתוך Hooks מותאמים אישית, אתה יכול להסתיר את הפרטים המסובכים של איך אתה מתמודד עם מערכת חיצונית כלשהי או API של דפדפן. הקוד של הרכיבים שלך מבטא את כוונתך, לא את היישום.

שמות Hook תמיד מתחילים ב-use

יישומי React בנויים מרכיבים. רכיבים בנויים מ-Hooks, בין אם מובנים או מותאמים אישית. סביר להניח שלעתים קרובות תשתמש ב-Hooks מותאמים אישית שנוצרו על ידי אחרים, אבל לפעמים אתה עשוי לכתוב אחד בעצמך!

עליך לפעול לפי מוסכמות השמות הבאות:

  1. שמות רכיבי React חייבים להתחיל באות גדולה, כמו ‘StatusBar’ ו-‘SaveButton’. רכיבי React צריכים גם להחזיר משהו שראקט יודע להציג, כמו חתיכת JSX.
  2. שמות Hook חייבים להתחיל ב-use ואחריו באות גדולה, כמו useState (מובנה) או useOnlineStatus (מותאם אישית, כמו קודם בדף). הHooks עשויים להחזיר ערכים שרירותיים.

מוסכמה זו מבטיחה שתמיד תוכל להסתכל על רכיב ולדעת היכן מצבו, אפקטים ושאר תכונות React עשויות “להסתתר”. לדוגמה, אם אתה רואה קריאה לפונקציה getColor() בתוך הרכיב שלך, אתה יכול להיות בטוח שהיא לא יכולה להכיל מצב React בפנים כי השם שלה לא מתחיל ב-use. עם זאת, קריאת פונקציה כמו useOnlineStatus() תכיל ככל הנראה קריאות ל-Hooks אחרים בפנים!

Note

אם ה-linter שלך הוא מוגדר עבור React, הוא יאכוף את מוסכמות השמות הזו. גלול למעלה אל ארגז החול למעלה ושנה את השם של ‘useOnlineStatus’ ל’getOnlineStatus’. שימו לב שה-linter לא יאפשר לכם לקרוא ‘useState’ או ‘useEffect’ בתוכו יותר. רק הHooks ורכיבים יכולים לקרוא לHooks אחרים!

Deep Dive

האם כל הפונקציות שנקראות במהלך העיבוד צריכות להתחיל בקידומת השימוש?

לא. פונקציות שלא קוראות לHooks לא צריכות להיות Hooks.

אם הפונקציה שלך לא קוראת לאף Hooks, הימנע מקידומת ‘שימוש’. במקום זאת, כתוב אותה כפונקציה רגילה ללא הקידומת ‘שימוש’. לדוגמה, ‘useSorted’ למטה לא קורא Hooks, אז קרא לזה ‘getSorted’ במקום זאת:

// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}

זה מבטיח שהקוד שלך יכול לקרוא לפונקציה הרגילה הזו בכל מקום, כולל תנאים:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ It's ok to call getSorted() conditionally because it's not a Hook
displayedItems = getSorted(items);
}
// ...
}

עליך לתת קידומת ‘use’ לפונקציה (ובכך להפוך אותה ל-Hook) אם היא משתמשת לפחות Hook אחד בתוכה:

// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}

מבחינה טכנית, זה לא נאכף על ידי React. באופן עקרוני, אתה יכול לעשות Hook שלא קורא לHook אחרים. זה לעתים קרובות מבלבל ומגביל ולכן עדיף להימנע מהדפוס הזה. עם זאת, ייתכנו מקרים נדירים שבהם זה מועיל. לדוגמה, אולי הפונקציה שלך לא משתמשת באף Hooks כרגע, אבל אתה מתכנן להוסיף לה כמה שיחות Hook בעתיד. אז הגיוני לתת לזה שם עם הקידומת ‘שימוש’:

// ✅ Good: A Hook that will likely use some other Hooks later
function useAuth() {
// TODO: Replace with this line when authentication is implemented:
// return useContext(Auth);
return TEST_USER;
}

אז רכיבים לא יוכלו לקרוא לזה מותנה. זה יהפוך חשוב כאשר אתה באמת מוסיף שיחות Hook פנימה. אם אינכם מתכננים להשתמש בHooks בתוכו (עכשיו או מאוחר יותר), אל תהפכו אותו לHook.

ווים מותאמים מאפשרים לך לשתף היגיון מצבי, לא להגדיר את עצמו

בדוגמה הקודמת, כאשר הפעלת וכיבית את הרשת, שני הרכיבים עודכנו יחד. עם זאת, זה לא נכון לחשוב שמשתנה מצב ‘isOnline’ יחיד משותף ביניהם. תסתכל על הקוד הזה:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

זה עובד באותו אופן כמו לפני שחילצת את הכפילות:

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

אלו שני משתני מצב ואפקטים בלתי תלויים לחלוטין! במקרה היה להם אותו ערך בו-זמנית כי סינכרנתם אותם עם אותו ערך חיצוני (בין אם הרשת מופעלת).

כדי להמחיש זאת טוב יותר, נצטרך דוגמה אחרת. שקול את רכיב ה’טופס’ הזה:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

יש היגיון שחוזר על עצמו עבור כל שדה טופס:

  1. יש פיסת state (שם פרטי ושם משפחה).
  2. יש מטפל בשינוי (handleFirstNameChange ו-handleLastNameChange).
  3. יש קטע של JSX שמציין את התכונות ‘ערך’ ו-‘onChange’ עבור הקלט הזה.

אתה יכול לחלץ את ההיגיון החוזר על ה-Hook המותאם אישית של ‘useFormInput’:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

שימו לב שהוא מכריז רק על משתנה מצב אחד בשם ‘ערך’.

עם זאת, הרכיב ‘Form’ קורא ‘useFormInput’ פעמיים:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

זו הסיבה שזה עובד כמו הכרזה על שני משתני מצב נפרדים!

Custom Hooks מאפשרים לך לשתף היגיון מצבי אבל לא לציין את עצמו. כל קריאה ל-Hook עצמאית לחלוטין מכל קריאה אחרת לאותו Hook. זו הסיבה ששתי ארגזי החול שלמעלה שוות לחלוטין. אם תרצה, גלול חזרה למעלה והשווה ביניהם. ההתנהגות לפני ואחרי חילוץ Hook מותאם אישית זהה.

כאשר אתה צריך לשתף את הstate עצמו בין רכיבים מרובים, הרם אותו והעביר אותו למטה במקום זאת.

העברת ערכים תגובתיים בין Hooks

הקוד בתוך Hooks המותאמים אישית שלך יפעל מחדש במהלך כל רינדור מחדש של הרכיב שלך. זו הסיבה, כמו רכיבים, Hooks מותאמים אישית צריכים להיות טהורים. חשבו על הקוד המותאם אישית של Hooks כחלק מגוף הרכיב שלכם!

מכיוון שה-Hooks מותאמים אישית מעבדים מחדש יחד עם הרכיב שלך, הם תמיד מקבלים את הprops וstate העדכניים ביותר. כדי לראות מה זה אומר, שקול את הדוגמה הזו לחדר צ’אט. שנה את כתובת האתר של השרת או את חדר הצ’אט:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

כאשר אתה משנה serverUrl או roomId, האפקט “מגיב” לשינויים שלך ומסתנכרן מחדש. אתה יכול לראות לפי הודעות המסוף שהצ’אט מתחבר מחדש בכל פעם שאתה משנה את התלות של האפקט שלך.

כעת העבר את הקוד של האפקט ל-Hook מותאם אישית:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

זה מאפשר לרכיב ChatRoom שלך לקרוא ל-Hook המותאם אישית שלך מבלי לדאוג איך זה עובד בפנים:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
Server URL:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}

זה נראה הרבה יותר פשוט! (אבל זה עושה את אותו הדבר.)

שימו לב שההיגיון עדיין מגיב לשינויי תמיכה וstate. נסה לערוך את כתובת האתר של השרת או את החדר שנבחר:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

שים לב איך אתה לוקח את ערך ההחזר של Hook אחד:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

והעבירו אותו כקלט לHook אחר:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

בכל פעם שרכיב ChatRoom שלך מעבד מחדש, הוא מעביר את roomId ואת serverUrl העדכניים ביותר ל-Hook שלך. זו הסיבה שהאפקט שלך מתחבר מחדש לצ’אט בכל פעם שהערכים שלהם שונים לאחר עיבוד מחדש. (אם אי פעם עבדת עם תוכנת עיבוד אודיו או וידאו, שרשור Hooks כזה עשוי להזכיר לך שרשור אפקטים ויזואליים או אודיו. זה כאילו הפלט של useState “מוזן” לקלט של useChatRoom.)

העברת מטפלי אירועים ל-Hooks מותאמים אישית

Under Construction

סעיף זה מתאר API ניסיוני שעדיין לא שוחרר בגרסה יציבה של React.

ככל שתתחיל להשתמש ב-‘useChatRoom’ ברכיבים נוספים, אולי תרצה לאפשר לרכיבים להתאים אישית את ההתנהגות שלו. לדוגמה, נכון לעכשיו, ההיגיון של מה לעשות כשמגיעה הודעה מקודד בתוך ה-Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

נניח שאתה רוצה להעביר את ההיגיון הזה בחזרה לרכיב שלך:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...

כדי לגרום לזה לעבוד, שנה את ה-Hook המותאם אישית שלך כדי לקחת את ‘onReceiveMessage’ כאחת מהאפשרויות הנקראות שלו:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}

זה יעבוד, אבל יש עוד שיפור אחד שאתה יכול לעשות כאשר ה-Hook המותאם אישית שלך מקבל מטפלי אירועים.

הוספת תלות ב-‘onReceiveMessage’ אינה אידיאלית מכיוון שהיא תגרום לצ’אט להתחבר מחדש בכל פעם שהרכיב מעבד מחדש. עטפו את מטפל האירוע הזה לתוך אירוע אפקט כדי להסיר אותו מהתלות:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
}

כעת הצ’אט לא יתחבר מחדש בכל פעם שרכיב ChatRoom מעבד מחדש. הנה הדגמה עובדת מלאה של העברת מטפל באירועים לHook מותאם אישית שתוכל לשחק איתו:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

שים לב איך אתה כבר לא צריך לדעת איך useChatRoom עובד כדי להשתמש בו. אתה יכול להוסיף אותו לכל רכיב אחר, להעביר כל אופציה אחרת, וזה יעבוד באותה צורה. זה הכוח של הHooks מותאמים אישית.

מתי להשתמש ב-Hooks מותאמים אישית

אינך צריך לחלץ Hook מותאם אישית עבור כל פיסת קוד משוכפלת קטנה. כמה כפילות זה בסדר. לדוגמה, חילוץ useFormInput Hook כדי לכרוך קריאת useState בודדת כמו קודם הוא כנראה מיותר.

עם זאת, בכל פעם שאתה כותב אפקט, שקול אם יהיה ברור יותר לעטוף אותו גם בHook מותאם אישית. לא צריך אפקטים לעתים קרובות מאוד, אז אם אתה כותב אחד, זה אומר שאתה צריך “לצאת מחוץ ל-React” כדי להסתנכרן עם מערכת חיצונית כלשהי או לעשות משהו של-React אין API מובנה עבורו. לעטוף אותו לתוך Hook מותאם אישית מאפשר לך לתקשר במדויק את כוונתך וכיצד הנתונים זורמים דרכו.

לדוגמה, שקול רכיב ShippingForm המציג שתי תפריטים נפתחים: אחד מציג את רשימת הערים, ואחר מציג את רשימת האזורים בעיר שנבחרה. אתה יכול להתחיל עם איזה קוד שנראה כך:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// This Effect fetches cities for a country
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// This Effect fetches areas for the selected city
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

למרות שהקוד הזה די חוזר על עצמו, נכון לשמור על אפקטים אלה נפרדים זה מזה. הם מסנכרנים שני דברים שונים, אז לא כדאי למזג אותם לאפקט אחד. במקום זאת, אתה יכול לפשט את רכיב ShippingForm למעלה על ידי חילוץ ההיגיון המשותף ביניהם לתוך ה-useData שלך:

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

כעת אתה יכול להחליף את שני האפקטים ברכיבי ShippingForm בקריאות ל-useData:

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

חילוץ Hook מותאם אישית הופך את זרימת הנתונים למפורשת. אתה מזין את ה’כתובת’ פנימה ואתה מוציא את ה’נתונים’. על ידי “הסתרת” האפקט שלך בתוך useData, אתה גם מונע ממישהו שעובד על רכיב ShippingForm להוסיף לו תלות מיותרת. עם הזמן, רוב האפקטים של האפליקציה שלך יהיו ב-Hooks מותאמים אישית.

Deep Dive

שמור את ה-Hooks המותאמים אישית שלך ממוקדים במקרים של שימוש בטון ברמה גבוהה

התחל בבחירת השם המותאם אישית של Hook. אם אתה מתקשה לבחור שם ברור, זה עשוי לומר שהאפקט שלך מחובר מדי לשאר ההיגיון של הרכיב שלך, ועדיין אינו מוכן לחילוץ.

באופן אידיאלי, השם של ה-Hook המותאם אישית שלך צריך להיות ברור מספיק כדי שאפילו אדם שלא כותב קוד לעתים קרובות יוכל לנחש היטב מה ה-Hook המותאם אישית שלך עושה, מה הוא דורש ומה הוא מחזיר:

  • useData(url)
  • useImpressionLog(eventName, extraData)
  • שימוש בצ'אט (אפשרויות)

כאשר אתה מסתנכרן עם מערכת חיצונית, השם Hook המותאם אישית שלך עשוי להיות טכני יותר ולהשתמש בז’רגון ספציפי לאותה מערכת. זה טוב כל עוד זה יהיה ברור לאדם שמכיר את המערכת הזו:

  • useMediaQuery(שאילתה)
  • useSocket(url)
  • useIntersectionObserver(ref, options)

שמור על הHooks מותאמים אישית ממוקדים במקרים של שימוש בטון ברמה גבוהה. הימנע מיצירה ושימוש ב-Hooks מותאמים אישית של “מחזור חיים” הפועלים כאלטרנטיבות ועטיפות נוחות עבור ממשק ה-API של ‘useEffect’ עצמו:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

לדוגמה, ה-‘useMount’ Hook הזה מנסה להבטיח שקוד מסוים רץ רק “על mount”:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔴 Avoid: using custom "lifecycle" Hooks
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Avoid: creating custom "lifecycle" Hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}

Hooks של “מחזור חיים” מותאם אישית כמו useMount אינם מתאימים היטב לפרדיגמת React. לדוגמה, בדוגמה של קוד זו יש טעות (הוא לא “מגיב” לשינויים של roomId או serverUrl), אבל ה-linter לא מזהיר אותך על כך מכיוון שה-linter בודק רק קריאות ישירות של ‘useEffect’. זה לא יידע על Hook שלך.

אם אתה כותב אפקט, התחל על ידי שימוש ישירות ב-React API:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Good: two raw Effects separated by purpose

useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

לאחר מכן, אתה יכול (אך לא חייב) לחלץ Hooks מותאמים אישית עבור מקרי שימוש שונים ברמה גבוהה:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Great: custom Hooks named after their purpose
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Hook מותאם אישית טוב הופך את הקוד הקורא להצהרתי יותר על ידי הגבלה של מה שהוא עושה. לדוגמה, useChatRoom(options) יכול להתחבר רק לחדר הצ’אט, בעוד ש-useImpressionLog(eventName, extraData) יכול לשלוח רק יומן הופעות לניתוח. אם ה-API המותאם אישית של Hook לא מגביל את מקרי השימוש והוא מאוד מופשט, בטווח הארוך הוא צפוי להציג יותר בעיות ממה שהוא פותר.

ווים מותאמים אישית עוזרים לך לעבור לדפוסים טובים יותר

אפקטים הם “פתח מילוט”: אתה משתמש בהם כאשר אתה צריך “לצאת החוצה React” וכאשר אין פתרון מובנה טוב יותר עבור מקרה השימוש שלך. עם הזמן, המטרה של צוות React היא לצמצם את מספר האפקטים באפליקציה שלך למינימום על ידי מתן פתרונות ספציפיים יותר לבעיות ספציפיות יותר. עטיפה של האפקטים שלך ב-Hooks מותאמים אישית מקלה על שדרוג הקוד שלך כאשר הפתרונות האלה יהיו זמינים.

נחזור לדוגמא הזו:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

בדוגמה שלמעלה, useOnlineStatus מיושם עם זוג של useState ו-useEffect. עם זאת, זה לא הפתרון הטוב ביותר האפשרי. יש מספר מקרי קצה שהיא לא לוקחת בחשבון. לדוגמה, הוא מניח שכאשר הרכיב נטען, isOnline כבר נכון, אבל זה עשוי להיות שגוי אם הרשת כבר יצאה לstate לא מקוון. אתה יכול להשתמש בממשק ה-API של הדפדפן navigator.onLine כדי לבדוק זאת, אך השימוש בו ישירות לא יעבוד בשרת ליצירת ה-HTML הראשוני. בקיצור, ניתן לשפר את הקוד הזה.

למרבה המזל, React 18 כולל API ייעודי בשם useSyncExternalStore שמטפל בכל הבעיות הללו עבורך. הנה איך ה-‘useOnlineStatus’ שלך נכתב מחדש כדי לנצל את ה-API החדש הזה:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

שימו לב כיצד לא היית צריך לשנות אף אחד מהרכיבים כדי לבצע את ההגירה הזו:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

זו סיבה נוספת לכך שעטיפה של אפקטים בHooks מותאמים אישית היא לעתים קרובות מועילה:

  1. אתה הופך את זרימת הנתונים אל האפקטים שלך וממנו בצורה מאוד מפורשת.
  2. אתה נותן לרכיבים שלך להתמקד בכוונה ולא ביישום המדויק של האפקטים שלך.
  3. כאשר React מוסיף תכונות חדשות, אתה יכול להסיר את האפקטים האלה מבלי לשנות אף אחד מהרכיבים שלך.

בדומה למערכת עיצוב, אולי יעזור לך להתחיל לחלץ ניבים נפוצים ממרכיבי האפליקציה שלך לתוך Hooks מותאמים אישית. זה ישמור על הקוד של הרכיבים שלך ממוקד בכוונה, ויאפשר לך להימנע מכתיבת אפקטים גולמיים לעתים קרובות מאוד. הHooks מותאמים אישית רבים מתוחזקים על ידי קהילת React.

Deep Dive

האם React יספק כל פתרון מובנה לאיסוף נתונים?

אנחנו עדיין עובדים על הפרטים, אבל אנו מצפים שבעתיד תכתוב איסוף נתונים כך:

import { use } from 'react'; // Not available yet!

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

אם אתה משתמש ב-Hooks מותאמים אישית כמו ‘useData’ למעלה באפליקציה שלך, זה ידרוש פחות שינויים כדי לעבור לגישה המומלצת בסופו של דבר מאשר אם תכתוב אפקטים גולמיים בכל רכיב באופן ידני. עם זאת, הגישה הישנה עדיין תעבוד בסדר, אז אם אתה מרגיש שמח לכתוב אפקטים גולמיים, אתה יכול להמשיך לעשות זאת.

יש יותר מדרך אחת לעשות את זה

נניח שאתה רוצה ליישם אנימציה דהייה מאפס באמצעות דפדפן requestAnimationFrame API. אתה יכול להתחיל עם אפקט שמגדיר לולאת אנימציה. במהלך כל פריים של האנימציה, אתה יכול לשנות את האטימות של צומת ה-DOM שאתה מחזיק ב-ref עד שהוא מגיע ל-1. הקוד שלך עשוי להתחיל כך:

import { useState, useEffect, useRef } from 'react';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // We still have more frames to paint
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

כדי להפוך את הרכיב לקריאה יותר, תוכל לחלץ את ההיגיון לתוך Hook מותאם אישית של ‘useFadeIn’:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

אתה יכול לשמור את הקוד ‘useFadeIn’ כפי שהוא, אבל אתה יכול גם לשחזר אותו יותר. לדוגמה, אתה יכול לחלץ את ההיגיון להגדרת לולאת האנימציה מתוך useFadeIn ל-useAnimationLoop Hook מותאם אישית:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

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

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

אפקטים מאפשרים לך לחבר את React למערכות חיצוניות. ככל שנדרש יותר תיאום בין אפקטים (לדוגמה, לשרשרת אנימציות מרובות), יותר הגיוני לחלץ את ההיגיון הזה מתוך אפקטים וHooks לגמרי כמו בארגז החול שלמעלה. לאחר מכן, הקוד שחילצת הופך ל”מערכת החיצונית”. זה מאפשר לאפקטים שלך להישאר פשוטים מכיוון שהם רק צריכים לשלוח הודעות למערכת שהעברת מחוץ ל-React.

הדוגמאות לעיל מניחות שהלוגיקת ה-Fade-in צריכה להיות כתובה ב-JavaScript. עם זאת, אנימציית הדה-אין הספציפית הזו פשוטה יותר והרבה יותר יעילה ליישום עם CSS Animation:

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

לפעמים, אתה אפילו לא צריך Hook!

Recap

  • ווים מותאמים אישית מאפשרים לך לשתף היגיון בין רכיבים.
  • יש לתת שם ל-Custom Hooks שמתחיל ב-‘use’ ואחריו באות גדולה.
  • ווים מותאמים אישית חולקים רק היגיון מצבי, לא מצב עצמו.
  • אתה יכול להעביר ערכים תגובתיים מHook אחד למשנהו, והם נשארים מעודכנים.
  • כל הHooks פועלים מחדש בכל פעם שהרכיב שלך מעבד מחדש.
  • הקוד של Hooks המותאמים אישית שלך צריך להיות טהור, כמו הקוד של הרכיב שלך.
  • עטוף מטפלי אירועים שהתקבלו על ידי Hooks מותאמים אישית לאירועי אפקט.
  • אל תיצור ווים מותאמים אישית כמו useMount. שמור על ייעודם ספציפי.
  • זה תלוי בך איך והיכן לבחור את גבולות הקוד שלך.

Challenge 1 of 5:
חלץ Hook של useCounter

רכיב זה משתמש במשתנה מצב ובאפקט כדי להציג מספר שגדל כל שנייה. חלץ את ההיגיון הזה לתוך Hook מותאם אישית בשם useCounter. המטרה שלך היא לגרום למימוש רכיב ‘Counter’ להיראות בדיוק כך:

export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}

תצטרך לכתוב את ה-Hook המותאם אישית שלך ב-useCounter.js ולייבא אותו לקובץ Counter.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}