راهنمای هوک های useState و useEffect در ری اکت

کتابخانه ری اکت در سال 2018 میلادی و از نسخه 16.8.0 به بعد قابلیت هوک (Hook) را معرفی کرد. تا قبل از این ورژن، این امکان را نداشتیم که از state در فانکشنال کامپوننت استفاده کنیم. در صورتیکه می خواستیم در یک functional component از state استفاده کنیم باید کد کامپوننت را ریفکتور می کردیم و به کلاس کامپوننت class component تبدیل می کردیم. اما یکی از قابلیت هایی که هوک های ری اکت در اختیار توسعه دهنده گان ریکت قرار می دهد امکان تعریف state در کامپوننت های تابعی یا functional component می باشد.
در این مقاله قصد داریم یک پروژه تستی ریکت ایجاد کنیم و دو هوک کاربردی و مهم useState و useEffect را تحت این پروژه بررسی کنیم.
تنظیمات اولیه پروژه:
برای اجرای دستورات npm نیاز است آخرین نسخه NodeJS روی سیستم عامل شما نصب باشد. سپس به منظور ایجاد یک پروژه ریکت خام دستور زیر را در cmd یا ترمینال اجرا کنید:
npx create-react-app rahkarino-hooks
مقدمه ای بر this.setState
ریکت یک کتابخانه برای ایجاد رابط های کاربری یا user interface می باشد. تئوری اساسی در ریکت به این شکل است که هر صفحه از وب اپلیکیشن از بخش های مجزا و کوچکی بنام کامپوننت تشکیل شده اند که هر یک از این component ها می توانند به تغییرات state واکنش یا ریکت نشان دهند.
مثال زیر را در این زمینه در نظر بگیرید. یک کلاس ES6 که از React.Component ارث بری می کند.
import React, { Component } from "react";
export default class Button extends Component {
constructor() {
super();
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(() => {
return { buttonText: "Thanks, been clicked!" };
});
}
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}در مثال فوق، در رویداد کلیک دکمه، state توسط this.setState فراخوانی و آپدیت می شود. در واقع متن دکمه یا button text به state واکنش نشان می دهد و مقدار buttonText آپدیت شده را بعنوان متن جدید دکمه در نظر می گیرد.
نکته: توسط هوک یا Hook در ریکت می توان همین لاجیک را بدون استفاده از کلاس های ES6 پیاده سازی کرد.
آپدیت کردن State بدون setState
سوال اینجاست که بدون کلاس های اکما اسکریپت 2015 و همچنین بدون استفاده از دستور this.setState چگونه می توان state اپلیکیشن را آپدیت کرد؟
لازم است ابتدا هوک useState را بصورت زیر در ابتدای کامپوننت ایمپورت کنیم:
import React, { useState } from "react";پس از ایمپورت کردن useState Hook در کامپوننت موردنظر، دو مقدار زیر به شکل destructure در اختیار شما قرار می گیرد:
const [buttonText, setButtonText] = useState("Click me, please")اگر قضیه desctructuring را متوجه نشدید، می توانید مبحث Array Destructuring را در ES 2015 مطالعه کنید.
اسامی buttonText و setButtonText دلخواه هستند. هر نامی را می توانید برای این دو مقدار تعریف کنید. فقط باید سعی کنید اسامی انتخابی به اندازه کافی توصیفی و واضح باشند.
آرگومانی که به useState پاس داده می شود در واقع بیانگر state اولیه یا initial می باشد. آرگومان های useState به صورت زیر هستند:
اولین آرگومان در این مثال buttonText مقدار واقعی state و دومین آرگومان در این مثال یعنی setButtonText بعنوان فانکشن آپدیت کننده state عمل می کند.
بنابراین اگر بخواهیم مثال قبل را با استفاده از هوک useState پیاده سازی کنیم داریم:
import React, { useState } from "react";
export default function Button() {
const [buttonText, setButtonText] = useState("Click me, please");
return (
<button onClick={() => setButtonText("Thanks, been clicked!")}>
{buttonText}
</button>
);
}دریافت داده از API توسط Fetch
قبل از معرفی هوک در ریکت، برای fetch کردن داده از API به شکل زیر عمل می کردیم:
import React, { Component } from "react";
export default class DataLoader extends Component {
state = { data: [] };
componentDidMount() {
fetch("http://localhost:3001/links/")
.then(response => response.json())
.then(data =>
this.setState(() => {
return { data };
})
);
}
render() {
return (
<div>
<ul>
{this.state.data.map(el => (
<li key={el.id}>{el.title}</li>
))}
</ul>
</div>
);
}
}کد فوق به هیچ وجه قابل استفاده مجدد یا REUSABLE نمی باشد. با استفاده از render prop می توان بسادگی دیتا را با کامپوننت های فرزند به اشتراک گذاشت:
import React, { Component } from "react";
export default class DataLoader extends Component {
state = { data: [] };
componentDidMount() {
fetch("http://localhost:3001/links/")
.then(response => response.json())
.then(data =>
this.setState(() => {
return { data };
})
);
}
render() {
return this.props.render(this.state.data);
}
}اکنون می توانیم از بیرون از این کامپوننت، render prop ها را پاس دهیم:
<DataLoader
render={data => {
return (
<div>
<ul>
{data.map(el => (
<li key={el.id}>{el.title}</li>
))}
</ul>
</div>
);
}}
/>اما این روش نیز معایب خودش را دارد. به همین دلایل توسعه دهندگان ریکت به این فکر افتادند که برای بهبود قابلیت reusable و نیز encapsulation هوک Hook را ارائه دهند.
یکی از کاربردی ترین هوک ها در ریکت useEffect است. در بخش زیر به بررسی این هوک می پردازیم:
دریافت داده توسط هوک useEffect:
کاری که هوک useEffect انجام می دهد همان کار متدهای componentDidMount, componentDidUpdate, componentWillUnmount می باشد. و تمام آنها را در یک API واحد ادغام کرده است.
حالا می خواهیم Dataloader بالا را توسط useEffect Hook پیاده سازی کنیم. کامپوننت به فانکشن تبدیل می شود و متد fetch در داخل useEffect فراخوانی می شود.
علاوه بر این، بجای استفاده از this.setState از هوک useState بصورت setData استفاده کرده ایم:
export default function DataLoader() {
const [data, setData] = useState([]);
useEffect(() => {
fetch("http://localhost:3001/links/")
.then(response => response.json())
.then(data => setData(data));
});
return (
<div>
<ul>
{data.map(el => (
<li key={el.id}>{el.title}</li>
))}
</ul>
</div>
);
}پس از اجرای برنامه، آن چیزی که در کنسول مرورگر مشاهده می کنیم بصورت زیر خواهد بود:

همانطور که می دانید متد componentDidUpdate یکی از متدهای چرخه حیات یا life cycle در ریکت می باشد. این متد زمانی اجرا می شود که کامپوننت یک prop جدید دریافت کند یا state کامپوننت تغییر کند. زمانیکه هوک useEffect صدا زده می شود، داخل یک لوپ بی نهایت می افتد. به شکل تصویر فوق.
برای برطرف شدن این باگ، ما باید یک آرایه خالی را بعنوان آرگومان دوم useEffect پاس دهیم. کد زیر:
//
useEffect(() => {
fetch("http://localhost:3001/links/")
.then(response => response.json())
.then(data => setData(data));
}, []); // << super important array
//این آرایه شامل وابستگی ها یا dependencies برای هوک useEffect می باشد. در واقع شرطی است که در صورت برقرار بودن آن، هوک useEffect مجددا اجرا خواهد شد. وقتی یک آرایه خالی پاس داده می شود می گوییم useEffect فقط یکبار اجرا شود.
UseEffect Cleanup:
تایمرها، listener ها و اتصال های مداوم یا persistent connections مانند وب سوکت WebSocket عوامل اصلی نشت حافظه یا memory leak در جاوا اسکریپت می باشند.
کد زیر را در نظر بگیرید. توسط هوک useEffect یک کانکشن به سرور Socket.IO برقرار کرده ایم:
useEffect(() => {
const socket = socketIOClient(ENDPOINT);
socket.on("FromAPI", data => {
setResponse(data);
});
}, []);مشکل کد فوق اینست که اتصال به سرور حتی پس از unmount شدن کامپوننت از DOM برقرار می ماند و قطع نمی شود. نکته ای که وجود دارد اینست که هوک useEffect می تواند در نهایت یک تابع return کند که می توان در آن effect را غیرفعال کرد یعنی در این مثال اتصال به سرور سوکت را قطع کرد.
این قضیه معادل متد componentWillUnmount در کلاس ها می باشد. با این تفاسیر کد ما به شکل زیر خواهد شد:
useEffect(() => {
const socket = socketIOClient(ENDPOINT);
socket.on("FromAPI", data => {
setResponse(data);
});
return () => socket.disconnect();
}, []);اکنون با این کد پس از unmount شدن کامپوننت، اتصال به سرور نیز قطع می شود.
استفاده از render prop در هوک ریکت
در کد زیر می خواهیم به کامپوننت Dataloader آرگومان props را پاس دهیم:
import React, { useState, useEffect } from "react";
export default function DataLoader(props) {
const [data, setData] = useState([]);
useEffect(() => {
fetch("http://localhost:3001/links/")
.then(response => response.json())
.then(data => setData(data));
}, []); // << super important array
return props.render(data)
}پس می توان در React Hook از props هم استفاده کرد.
تعریف هوک سفارشی (Custom Hook) در ریکت:
بجای استفاده از HOC یا Higher Order Component ها و render props می توان لاجیک موردنظر را در یک ریکت هوک سفارشی تعریف کرد و در هر جای برنامه که نیاز بود آن را import و استفاده کرد.
در این مثال می خواهیم یک هوک سفارشی در ریکت ایجاد کنیم که کارش fetch کردن data از سرور است. برای نام هوک های سفارشی بهتر است از کلمه use و سپس نام دلخواه استفاده کنیم. زیرا این روش نام گذاری یا naming convention در React Hook ها رعایت شده است. مانند useState یا useEffect
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, []);
return data;
}به شکل زیر می توان از هوک سفارشی useFetch استفاده کرد:
import React from "react";
import useFetch from "./useFetch";
export default function DataLoader(props) {
const data = useFetch("http://localhost:3001/links/");
return (
<div>
<ul>
{data.map(el => (
<li key={el.id}>{el.title}</li>
))}
</ul>
</div>
);
}و این آن چیزی است که باعث جذابیت و محبوبیت هوک های ری اکت شده است.
استفاده از async/await در useEffect
وقتی از هوک useEffect در ریکت استفاده می کنیم ممکن است بخواهیم از ساختار async/await در آن بهره ببریم. نگاهی به هوک سفارشی زیر بیاندازید:
// useFetch.js
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, []);
return data;
}برای پیاده سازی مفهوم promise و ساختار async/await در هوک های سفارشی مانند useFetch کد زیر را در نظر بگیرید:
// useFetch.js
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState([]);
useEffect(async () => {
const response = await fetch(url);
const data = await response.json();
setData(data);
}, []);
return data;
}کد فوق صحیح بنظر می رسد. اما پس از اجرای آن در کنسول یک ارور مطابق تصویر زیر خواهیم داشت:

کد فوق صحیح بنظر می رسد. اما پس از اجرای آن در کنسول یک ارور مطابق تصویر زیر خواهیم داشت:
پس از سرچ متن خطا در گوگل و تحقیق درباره آن متوجه می شویم که یک Promise نمی تواند از هوک useEffect بازگشت داده شود.
توابع ناهمگام یا async جاوا اسکریپتی یک Promise برمی گردانند و هوک useEffect باید یک فانکشن دیگر return کند که برای clean-up کردن effect بکار می رود.
پس از تغییرات مذکور و پیاده سازی مجدد هوک useEffect کد زیر را خواهیم داشت:
// useFetch.js
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [data, setData] = useState([]);
async function getData() {
const response = await fetch(url);
const data = await response.json();
setData(data);
}
useEffect(() => {
getData();
}, []);
return data;
}اکنون با این تغییرات جدید، هوک useEffect بخوبی با توابع promise و ساختار async/await کار می کند.
جمع بندی:
قابلیت هوک در ریکت در ماه نوامبر سال 2018 میلادی ارائه شد و در ری اکت ورژن 16.8 پیاده سازی شد. کتابخانه ریکت تعدادی هوک بصورت پیش فرض دارد (pre-defined hook). اما مهم ترین هوک ها در ری اکت useStateو useEffect هستند.
هوک useState امکان استفاده از state لوکال را در کامپوننت های ریکت را فراهم می کند. بدون اینکه نیاز به تعریف کلاس های اکما اسکریپت 2015 یا ES6 باشد.
هوک useEffect جایگزین سه متد componentDidMount, componentDidUpdate, componentWillUnmount می باشد که بصورت یک API واحد و تکی کار می کند.
در نهایت با استفاده از هوک Hook در ری اکت سه روش کلی برای تعریف کامپوننت داریم:
- فانکشنال کامپوننت (Functional Component)
- کلاس کامپوننت (Class Component)
- فانکشنال کامپوننت به همراه هوک (Hook)
لیست کامل هوک ها در ریکت:
(مرجع: reactjs.org)
برای مطالعه بیشتر در زمینه هوک های ریکت به مرجع اصلی کتابخانه ریکت یعنی Reactjs.org مراجعه کنید.
مطالب زیر را حتما مطالعه کنید
4 دیدگاه
به گفتگوی ما بپیوندید و دیدگاه خود را با ما در میان بگذارید.

بسیار عالی بود .ممنون از شما
خواهش میکنم 🙂
مقاله کامل و عالیی بود
ممنون
خواهش می کنم. ممنون از شما که مقاله را مطالعه کردید