Hooks
در اینجا انواع مختلف hook های ریاکت و تعریفشون رو داریم
تعریف
hook به معنی قلاب هست. از ورژن 16.8 به بعد ریاکت همراه با functional components معرفی شده. اکثر hook ها با عبارت use شروع میشوند و وظایف مختلفی دارند
useState
این hook در صورت تغییر باعث رندر مجدد کل کامپوننتی که داخلش هستیم میشود. اما با هربار تغییر مقدار خودش ریست نمیشود. بلکه صرفا DOM مجدد رندر میشود و مقدار خودش حفظ میشود
const state = useState(initialValue) // initialValue میتونه هر دیتا تایپی باشه
constole.log(state) // [initialValue, Fn]
state[1](newValue) // state = [newValue, Fn]
همونطور که قابل مشاهده هست خود state شامل یک آرایه با ۲ عضو هست. عضو اول مقدار حال حاضر این state را نشان میدهد و عضو دوم تابع تغییر این state است
عضو دوم آرایه وظیفهی تغییر عضو اول رو داره. نکته این هست که با هربار تغییر عضو اول DOM مجدد رندر میشود
Better Syntax
سینتکس معمول و بهتری که برای تعریف useState استفاده میکنند با استفاده از destructuring است
const [state, setState] = useState(initialValue)
console.log(state) // initialValue
Closure in States
ما اگر ۳ بار پشت سر هم مقدار setState را کال کنیم فقط یکی از آنها اجرا میشود.
دلیل این فرآیند این هست که ساختار state یک ساختار Closure هست و فقط به متغیرهای درونی خودش دسترسی دارد.
برای رفع این مشکل باید از callback function درون setState استفاده کرد.
یعنی مقدار قبلی state را دریافت کرد و تغییرات را مستقیم روی آن اعمال کرد.
const [count, setCount] = useState(0);
const increaseBy3 = () =>{
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
console.log(count) // 1
همانطور که مشاهده میشود صرفا یکبار مقدار count اشافه شده که این به این دلیل است که هربار react تابع setCount را کال میکند ب مقدار درونی Closure count دسترسی دارد که برابر ۰ است
برای حل این اشکال باید setState مدنظر را به صورت یک callback function کال کرد که بتوان مقدار حاضر حاظر state را دریافت کرد
هرمقداری که callback function برگردونه (return) مقدار جدید state میشود
const [count, setCount] = useState(0);
const increaseBy3 = () =>{
setCount((prevCount) => (prevCount + 1))
setCount((prevCount) => (prevCount + 1))
setCount((prevCount) => (prevCount + 1))
}
increaseBy3() // 3
یک مثال پیچیده
یک id, value میگیره و مقدار سن کاربر متناظر با id رو به مقدار value تا اضافه میکنه
const [users, setUsers] = useState([
{
id: 12,
name: "amirhossein",
lastname: "abazari",
age: 20
},
{
id: 13,
name: "ali",
lastname: "abazari",
age: 45
}
// ...
])
const increaseAge = (id, value) => {
setUsers((prevUsers) => {
return prevUsers.map(user => (user.id == id ? { ...user, age: user.age + value } : user))
})
}
increaseAge(12, 4)
/*
{
id: 12,
name: "amirhossein",
lastname: "abazari",
age: 24
},
{
id: 13,
name: "ali",
lastname: "abazari",
age: 45
}
// ...
*/
useEffect
-
این هوک برای بررسی تغییرات state های صفحهی ما استفاده میشود
-
به عبارت دیگر اگر state مدنظر ما تغییر بکند useEffect مرتبط به آن کال میشود
-
useEffect شامیل یک Life Cycle ۳ مرحلهای است
Life Cycle
-
Mount : برای اولین بار که صدا زده میشود
-
Update : هربار که state مدنظر ما تغییر میکند
-
Unmount
- زمانی اتفاق میافتد که کامپوننت مدنظر صفحه حذف شود یا دیگر render نشود
- قبل از هربار انجام عملیات update درون lifecycle یکبار مقدار unmount صدا زده میشود
How to Use
import { useEffect } from "react"
const Component = () =>{
useEffect(() => {
// 1
return () => {
// 2
}
}, [updateListener])
return (<>...</>)
}
بخش ۱ برای اولین بار هنگام mount کامپوننت صدا زده میشود
پس از mount اولیه هربار که updateListener تغییر بکند ابتدا بخش ۲ و سپس بخش ۱ صدا زده میشود.
پس درکل روند تغییرات useEffect مانند نمونهی زیر است
- Mount - بخش ۱
- تغییر
updateListener - Unmount - بخش ۲
- Updating Mount - بخش ۱
- تغییر
updateListener - ...
اگر جای updateListener هیچ چیزی قرار ندیم صرفا هنگام mount کل کامپوننت این useEffect کال میشود
یعنی تنها وابسته به کل کامپوننت و رندر اون هست
Unmount
به Unmount useEffect عبارت clean up هم استفاده میشود
از unmount زمانی استفاده میشود که کامپوننت از حالت رندر دربیاد، وارد صفحهی دیگری بشیم و یا قبل از هربار update شدن
یک مثال کاربردی از useEffect Unmount
برای مثال فرض کن با یک input داریم که هربار کاربر مقدار داخلش رو عوض میکنه یک ریکوست به سرور ما میفرستیم. در این حالت ممکنه خیلی از ریکوستها بیهوده باشه
در این حالت ما ریکوست قبلی کاربر رو هنگام آپدیت یک state ایگنور و abort میکنیم
import {useState, useEffect} from "react"
const Component = () => {
const [id, setID] = useState(null);
const controller = new AbortController();
useEffect(() => {
fetch(`url/${id}`, {signal:controller.signal})
return () =>{
controller.abort()
}
}, [id]);
return (
<input type="number" value={id} onChange={(e) => {
setID(e.target.value)
}}/>
)
}
توی این نمونه ما یک signal به درخواست fetch اختصاص میدیم و قبل از اجرای هر آپدیت میدانیم تابع unmount صدا میشود.
پس قبل از ارسال درخواست جدید درخواست قبلی را abort میکنیم
این باعث میشه لود روی سرور خیلی کمتر بشه.
useReducer
یکی از state های ریاکت هست که تغییرش باعث رندر مجدد کامپوننت میشه اما فرمت و عملکردش متفاوت از useState هست
const [state, dispatch] = useReducer(reducer, initialState)
تعریف یک useReducer مانند نمونهی بالا هست. که با state میتونیم مقدار حال حاضر رو بخونیم.
dispatch یک action دریافت میکند و مقدار قبلی state را به همراه این action به تابع reducer پاس میدهد و تابع reducer باتوجه به نوع action تغییرات را اعمال میکند
هرمقداری که تابع reducer برگردونه (return بکنه) درون state قرار میگیره.
initialState هم مقدار اولیهی این state است که هر دیتاتایپی میتواند باشد
Implementation
فرض کن یک counter داریم که ۳ عمل میتونیم روش انجام بدیم. ۱. افزایش سه واحدی ۲. کاهش پنج واحدی ۳. صفر کردن counter
import { useReducer } from "react";
const reducer = (state, action) =>{
switch (action.type){
case ("increase") :
return state + action.payload
case ("decrease") :
return state - action.payload
case ("reset") :
return 0
}
}
const Component = () =>{
const [counter, dispatch] = useReducer(reducer, 0)
return(
<div>
<p>{counter}</p>
<button onClick={()=>{dispatch({type : "increase", payload:3})}}>Increase 3</button>
<button onClick={()=>{dispatch({type : "decrease", payload:5})}}>Decrease 5</button>
<button onClick={()=>{dispatch({type : "reset"})}}>Reset</button>
</div>
)
}
روند اجرای useReducer به این صورت است که ابتدا تابع dispatch با یک ورودی به نام action کال میشود که معمولا فرمت ورودی استاندارد dispatch مانند شکل زیر است
{
"type": "...",
"payload": {
/* ... */
}
}
پس از کال کردن dispatch مستقیما مقدار action و state حال حاضر به تابع reducer پاس داده میشود.
حال ما به ازای action های متفاوت در dispatch باید در تابع reducer اونارو هندل کنیم که معمولا با switch case این اتفاق میافتد
درنهایت هرمقداری که توسط تابع reducer برگردانده شود به عنوان مقدار جدید state قرار میگیرد
تفاوت useReducer و useState
useReducer مفاهیمی مانند dispatch کردن را برای حالات مختلف دارد درحالی که مفهومی پشت setState لزوما نیست
چه زمانی از useReducer به جای useState استفاده کنیم؟
- تعداد
actionها خیلی زیاد باشد یا شباهت بسیار زیادی باهم داشته باشن - برای api ها مناسب هست. چون میتونیم یک
actionبرای ارور داشته باشیم و ارورهندلینگ راحتتر باش - دیتاها سنگین باشن و نیاز به تعداد زیادی
useStateداشته باشیم درحالی کهreducerهمه تا حد خوبی یکسان است
useContext
یک state manager هست که درواقع از خیلی از مشکلات prop drilling جلوگیری میکنه

فرض کن توی این عکس یک prop داخل child4 هست و میخوایم اونو به child2 پاس بدیم. باید چیکار کنیم؟
- اونو ابتدا به child1 سپس به parent و سپس به child2 پاس بدیم. اما این وسط خیلی کد
boiler plateداریم چون اصلا parent و child1 نیازی به این دیتا ندارن. - روش دوم با استفاده از Context هست که درواقع ما Parent را به عنوان یک Provider تعریف میکنیم و دیتاهارو داخل اون قرار میدیم. سپس هر فرزند به هر دیتایی نیاز داشته باشه مستقیم از Parent میگیره
Provider and Consumer
در این مثال کامپوننت Parent به عنوان Provider هست که یعنی تامین کنندهی اطلاعات
کامپوننتهای child4 و child2 هم Consumer یا همان مصرف کنندهی اطلاعات هستن
Provider
import React, {useState} from "react"
const DataContext = React.createContext();
export {DataContext};
const Parent = () => {
const [data, setData] = useState({...})
return (
<DataContext value={[data, setData]}>
<Child1 />
<Child2 />
</DataContext>
)
}
حتمی حتمی باید مقداری که به Context پاس میدیم به عنوان prop باید برابر value باشه
درواقع Context ما فقط مقدار value رو میگیره ولی میتونیم توش هرچیزی قرار بدیم. برای مثال value میتونه یک آبجکت بسیار تو در تو و پیچیده باشه
مقدار DataContext رو ما export میکنیم که در کامپوننتهای Consumer از اون بتونیم استفاده بکنیم
در این حالت هرچی که داخل DataContext پیچیده شده است دسترسی به مقدار value دارد
Consumer
import {useContext} from "react";
import {DataContext} from "./Parent"
const Child4 = () => {
const [data, setData] = useContext(DataContext)
return (
<input type="text" onChange={(e)=>{setData(e.target.value)}} value={data} />
)
}
درواقع ما در این کامپوننت دیتا را از parent به صورت destructuring میگیریم و مستقیم روش تغییر اعمال میکنیم و حتی نمایش میدیم. اما باید توجه داشته باشیم این تغییرات مستقیما روی Parent اعمال میشه و پاس داده میشه به فرزندان
import {useContext} from "react";
import {DataContext} from "./Parent"
const Child2 = () => {
const value = useContext(DataContext)
return (
<p>{value[0]}</p>
)
}
توی این نمونه ما بدون destructuring دیتا رو گرفتیم و گفتیم عضو اولش رو نشون بده. دقت کن که عضو دوم یک تابع هست و ما اینجا چون تغییر نمیدادیمش کاری باهاش نداشتیم