هوک ها در زندگی روزمره<!-- --> - Husen's Blog

هوک ها در زندگی روزمره

useCallback

یک تابع را به عنوان پارامتر اول می پذیرد . یک نسخه ذخیره شده از آن را برمیگرداند (از نظر مکان حافظه ذخیره می شود نه محاسبات داخل تابع) و نتیجتا هربار که کامپوننت ما دوباره رندر می شود تابع برگردانده شده دوباره ایجاد نمی شود در حالی که در توابع عادی همچین اتفاقی میوفتد.

اگر یکی از متغیرهای داخل آرایه وابستگی useCalback (پارامتر دوم آن) تغییر کند، تابع برگشتی در آن مرجع حافظه خود دوباره ایجاد می شود (recreated).

دربعضی مواقع ممکن است useEffect بصورت بینهایت فراخوان شود و در هر فراخوان یک نسخه جدید از آن ایجاد شود، بنابراین useCallback برای به خاطر سپردن آن استفاده می شود.

باید بدانید که ذخیره کردن (memoizing) رایگان نیست و اشتباه و بی دلیل استفاده کردن از آن به ضرر است.

useMemo نیز دقیقا همین عملیات را انجام می دهد. فقط با این تفاوت که بجای تابع، مقادیر ارجاع شده مانند آبجکت ها و آرایه ها یا هر مقداری که از یک محاسبه سنگین بدست آمده و نمی خواهیم دوباره تکرارش کنیم را می پذیرد.

useEffect

side effect به سه مورد تقسیم می شود:

  • تعامل با API و Backend
  • تعامل با API های Browser
  • استفاده از توابع زمانبندی شده مثل setTimeout و setInterval

کامپوننت های React بصورت تابع خالص نوشته شده اند یعنی شامل side effect نیستند. به مثال نباید از همچین چیزی داخل تابع کامپوننت استفاده کنیم:

function User({ name }) {
  document.title = name; 
  // This is a side effect. Don't do this in the component body!
    
  return <h1>{name}</h1>;   
}

اگر ما یک side effect را مستقیماً در بدنه کامپوننت خود داشته باشیم، در مسیر رندر کامپوننت React ما قرار می گیرد. side effect باید از فرآیند رندر جدا شود. اگر ما نیاز به انجام یک side effect داریم، باید جوری تنظیم کرد که بعد از رندر کامپوننت ما، انجام گیرد.

به طور خلاصه، useEffect ابزاری است که به ما امکان می دهد با دنیای بیرون تعامل داشته باشیم اما بر رندر یا عملکرد کامپوننتی که در آن قرار دارد تأثیری نمی گذارد.

نیتکس پایه useEffect به این صورت است: useEffect(() => {}, []); که تابعی که ارسال می شود یک تابع callback است. این پس از رندر شدن کامپوننت فراخوانی می شود. داخل این تابع می توانیم یک یا چند side effect استفاده کنیم. آرگومان دوم یک آرایه است که به آن آرایه وابستگی ها می گویند. این آرایه باید شامل تمام مقادیری باشد که side effect ما بر آنها تکیه دارد. کاری که این آرایه انجام می دهد این است که بررسی می کند و می بیند که آیا مقداری بین رندرها تغییر کرده است یا خیر. اگر چنین است، تابع useEffect ما را دوباره اجرا می کند.

اگر تابع وابستگی تعریف نکنید و فقط یک تابع ارائه دهید، آن تابع پس از هر رندر اجرا خواهد شد. و اگر داخل تابع state را تغییر دهید، پس از اولین رندر، useEffect اجرا می‌شود، سپس state به‌روزرسانی می‌شود، که باعث رندر مجدد می‌شود که باعث می‌شود useEffect دوباره اجرا شود و روند دوباره تا بی‌نهایت ادامه پیدا کند.

function MyComponent() {
  const [data, setData] = useState([])  
    
  useEffect(() => {
    fetchData().then(myData => setData(myData))
    // Error! useEffect runs after every render without the dependencies array, causing infinite loop
  }); 
}

اگر می خواهید تابع فقط یکبار اجرا شود و state بروزرسانی شود باید آرایه وابستگی خالی ارسال کرد.

function MyComponent() {
  const [data, setData] = useState([])  
    
  useEffect(() => {
    fetchData().then(myData => setData(myData))
    // Correct! Runs once after render with empty array
  }, []); 
   
  return <ul>{data.map(item => <li key={item}>{item}</li>)}</ul>
}

ممکن است در بعضی مواقع با این ارور مواجه بشیم:

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

پیام واضح است. ما درحالی که کامپوننت unmount و از دسترس خارج شده، سعی در تغییر state یک کامپوننت داریم. دلایل متفاوتی ممکن است وجود داشته باشد، رایج ترین آن این است که ما اشتراک یک کامپوننت websocket را لغو نکردیم، یا قبل از پایان عملیات async، آن را لغو کردیم.

useEffect به گونه ای ساخته شده که اگر داخل تابع آن تابعی را برگردانیم، در صورتی که ماپوننت از دسترس خارج شود آن تابع فراخوان می شود. این بسیار مفید است زیرا می توانیم از آن برای حذف رفتارهای غیر ضروری یا جلوگیری از مسائل مربوط به memory leaking استفاده کنیم.

بنابراین، اگر بخواهیم یک اشتراک را پاک کنیم، کد به شکل زیر خواهد بود:

useEffect(() => {
    API.subscribe()
    return function cleanup() {
        API.unsubscribe()
    }
})

یکی از پیاده سازی های متداول این است که به محض اتمام یک تابع async، وضعیت یا state کامپوننت را به روز کنید. اما چه اتفاقی می افتد اگر کامپوننت پس از اتمام کار از دسترس خارج شود؟ اگر ما آن را کنترل نکنیم، تلاش خواهد کرد به هر حال state را تنظیم و تغییر دهد. (به عنوان مثال قبل اینکه یک فرایندی تمام شود کاربر آن صفحه را ترک کند)

در مثال زیر، ما یک تابع async داریم که عملیاتی را انجام می‌دهد و در حین اجرا، من می‌خواهم یک پیام loading را ارائه دهم. پس از اتمام عملکرد، state "بارگذاری" را تغییر می دهم و پیام دیگری را نمایش می دهم.

function Example(props) {
    const [loading, setloading] = useState(true)

    useEffect(() => {
        fetchAPI.then(() => {
            setloading(false)
        })
    }, [])

    return <div>{loading ? <p>loading...</p> : <p>Fetched!!</p>}</div>
}

اما اگر ما کامپوننت را ترک کنیم و fetchAPI هم به اتمام برسد و loading state را تنظیم کند، این باید بروز ارور خواهد شد. ما باید به طریقی مطمعن باشیم که زمانی که fetchAPI تمام می شود کامپوننت هنوز در دسترس است.

function Example(props) {
    const [loading, setloading] = useState(true)

    useEffect(() => {
        let mounted = true
        fetchAPI.then(() => {
            if (mounted) {
                setloading(false)
            }
        })

        return function cleanup() {
            mounted = false
        }
    }, [])

    return <div>{loading ? <p>loading...</p> : <p>Fetched!!</p>}</div>
}

به این ترتیب می توانیم بفهمیم که آیا کامپوننت هنوز در دسترس است یا خیر. فقط یک متغیر اضافه می کنیم که در صورت خارج شدن از دسترس به false تغییر می کند.

یک مثال از لغو شدن درخواست Axios:

useEffect(() => {
    const source = axios.CancelToken.source()

    const fetchUsers = async () => {
        try {
            await Axios.get('/users', {
                cancelToken: source.token,
            })
            // ...
        } catch (error) {
            if (Axios.isCancel(error)) {
            } else {
                throw error
            }
        }
    }

    fetchData()

    return () => {
        source.cancel()
    }
}, [])

useRef

useRef باعث رندر شدن دوباره کامپوننت نمی شود. به فرض مثال بخواهیم تعداد رندر شدن یک کامپوننت را اندازه بگیریم و برای این امر از useState استفاده کنیم چون خود همین هوک باعث رندر شدن دوباره کامپوننت میشود ما در یک حلقه بی نهایت گیر خواهیم کرد. برای همین از useRef استفاده می کنیم که خود باعث رندر شدن دوباره کامپوننت نمی شود و خود در هر رندر اجرا می شود.

const [inputValue, setInputValue] = useState('')
const count = useRef(0)

useEffect(() => {
    count.current = count.current + 1
})

return (
    <>
         <input
            type="text"
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
        />
        <h1>Render Count: {count.current}</h1>
    </>
)

مورد دوم دسترسی به عناصر DOM با useRef می باشد که با اضافه کردن صفت ref به عنصر مربوطه.

const inputElement = useRef()

const focusInput = () => {
    inputElement.current.focus()
}

return (
    <>
        <input type="text" ref={inputElement} />
        <button onClick={focusInput}>Focus Input</button>
    </>
)

مورد آخر ردیابی تغییرات state است. ما می توانیم به ویژگی state قبلی دسترسی داشته باشیم. دلیل این کار این است که ما می توانیم مقادیر useRef را بین رندرها حفظ کنیم.

const [inputValue, setInputValue] = useState('')
const previousInputValue = useRef('')

useEffect(() => {
    previousInputValue.current = inputValue
}, [inputValue])

return (
    <>
        <input
            type="text"
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
        />
        <h2>Current Value: {inputValue}</h2>
        <h2>Previous Value: {previousInputValue.current}</h2>
    </>
)