از نسخه 16.8.0 ری اکت، قابلیت جدیدی به نام هوک (Hook) اضافه شد. هوک به بیان ساده مانند plugin در jquery است. همانطور که شما بدون اضافه شدن پیچیدگی به کدهای جاوا اسکریپت به راحتی از یک پلاگین jquery برای یک منظور خاص استفاده می کنید هوک ها هم تقریبا چنین نقشی را ایفا می کنند. هوک ها مانند پلاگین به اپلیکیشن اضافه می شوند. درست مانند قلاب های شکل بالا!
اجازه دهید به جای توضیح بیشتر در مورد مفاهیم هوک با پیاده سازی یک اپلیکیشن سرچ فیلم عملا با هوک ها آشنا شویم. اپلیکیشن پس از پیاده سازی به شکل زیر خواهد بود:
این اپ از API های OMDB برای دریافت لیست فیلم ها استفاده می کند. هدف از پیاده سازی این اپ این است که شما با کاربرد هوک در یک پروژه عملی آشنا شوید. به فولدری که قرار است پروژه را در آن قرار دهید رفته، command prompt را باز کرده و با دستور create-react-app یک پروژه ری اکت ایجاد کنید:
create-react-app searchmovieapp
حالا به فولدر searchmovieapp رفته، کلیک راست کرده و فولدر را با ویژوال استودیو کد باز کنید: ساختار پروژه مانند شکل زیر یعنی همان ساختار پیش فرضی است که با create-react-app ایجاد شده است:
در این پروژه طبق شکل نهایی آن که در بالا دیدیم، چهار کامپوننت خواهیم داشت. بنابراین اجازه دهید ببینیم هرکدام چه وظیفه و کارکردی خواهند داشت:
- App.js : این کامپوننت والد سه کامپوننت دیگر بوده و همچنین شامل توابع مربوط به صدا کردن API های OMDB است.
- Header.js : کامپوننتی ساده که هدر اپلیکیشن را ایجاد می کند و یک prop با نام title خواهد داشت که در هدر مقدار آن را نمایش می دهد.
- Movie.js : هر کدام از فیلم ها توسط این کامپوننت رندر می شوند؛ آبجکت movie به عنوان prop به آن پاس داده می شود.
- Search.js : این کامپونت شامل یک فیلد ورودی و یک دکمه بوده و شامل توابعی برای خواندن مقدار فیلد ورودی و ری ست کردن آن است. هم چنین شامل تابعی است که تابع سرچ را فراخوانی می کند.
در دایرکتوری src یک فولدر جدید با نام components ایجاد کنیم تا تمام کامپوننت ها را در آن قرار دهیم. سپس فایل App.js را به این فولدر انتقال می دهیم.
به فایل index.js رفته و import مربوط به App.js را اصلاح کنید تا مانند کد زیر از فولدر components بخواند (خط چهارم):
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
هم چنین برای تغییر ظاهر اپ محتوای زیر را در App.css جایگزین کنید (البته این میتواند مطابق سلیقه شما تغییر کند):
.App {
text-align: center;
}
.App-header {
background-color: #282c34;
height: 70px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
padding: 20px;
cursor: pointer;
}
.spinner {
height: 80px;
margin: auto;
}
.App-intro {
font-size: large;
color: #fff;
border-bottom: 1px solid #4e5054;
padding-bottom: 2rem;
}
* {
box-sizing: border-box;
}
body {
background: #363a42;
}
.movies {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: center;
}
.App-header h2 {
margin: 0;
}
.add-movies {
text-align: center;
}
.add-movies button {
font-size: 16px;
padding: 8px;
margin: 0 10px 30px 10px;
}
.movie {
padding: 5px 20px 10px 20px;
max-width: 25%;
color: #fff;
}
.errorMessage {
margin: auto;
font-weight: bold;
color: rgb(161, 15, 15);
}
.search {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-top: 10px;
}
input[type="submit"] {
padding: 10px;
background-color: #282c34;
color: white;
border: 1px solid #282c34;
width: 80px;
margin-left: 5px;
cursor: pointer;
}
h1 {
font-size: 1.4em;
}
h2 {
font-size: 1.3em;
}
input[type="submit"] {
font-family: tahoma;
}
input[type="submit"]:hover {
background-color: #282c34;
color: antiquewhite;
}
.search > input[type="text"]{
width: 40%;
min-width: 170px;
}
@media screen and (min-width: 694px) and (max-width: 915px) {
.movie {
max-width: 33%;
}
}
@media screen and (min-width: 652px) and (max-width: 693px) {
.movie {
max-width: 50%;
}
}
@media screen and (max-width: 651px) {
.movie {
max-width: 100%;
margin: auto;
}
}
حالا اجازه دهید کامپوننت Header را ایجاد کنیم. در فولدر components فایلی با نام Header.js بسازید و کد زیر را در آن قرار دهید:
import React from 'react'
const Header = (props) => {
return (
<header className="App-header">
<h1>{props.text}</h1>
</header>
)
}
export default Header
همانطور که می بینید این کامپوننت نیازی به توضیح ندارد و فقط تابعی است که یک تگ header را با محتوای text از props پر می کند.
حالا نوبت به کامپوننت Movie می رسد. فایل Movie.js را در فولدر components ایجاد کنید و کد زیر را در آن قرار دهید:
import React from "react";
const DEFAULT_PLACEHOLDER_IMAGE =
"https://images-na.ssl-images-amazon.com/images/I/714hR8KCqaL._SY355_.jpg";
const Movie = ({ movie }) => {
const poster =
movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster;
return (
<div className="movie">
<h2>{movie.Title}</h2>
<div>
<img width="200" alt={movie.Title} src={poster} />
</div>
<p>({movie.Year})</p>
</div>
);
};
export default Movie;
این کامپوننت هم یک کامپوننت تابعی است که برای نمایش هر فیلم در لیست فیلم ها ایجاد کرده ایم. ورودی این کامپوننت یک movie است که عنوان و سال تولید آن به علاوه پوستر آن نمایش داده می شود. اگر یک فیلم پوستر نداشت ، یک پوستر پیش فرض به جای آن نمایش داده می شود.
اجازه دهید سراغ اصلی ترین کامپوننت، یعنی Search برویم. قسمت جذاب ماجرا اینجاست. قبل از معرفی هوک، برای اینکه کامپوننتی دارای state داشته باشیم باید سراغ class component می رفتیم و کامپوننت های تابعی مانند همین Header و Movie که ایجاد کرده ایم توانایی مدیریت state داخلی در خودشان را نداشتند. اما به کمک هوک شما می توانید داخل یک کامپوننت از نوع تابع هم state داشته باشید.
فایلی با نام Search.js در فولدر کامپوننت ها ایجاد کنید و محتوای زیر را در آن قرار دهید:
import React, { useState } from "react";
const Search = (props) => {
const [searchValue, setSearchValue] = useState("");
const handleSearchInputChanges = (e) => {
setSearchValue(e.target.value);
}
const callSearchFunction = (e) => {
e.preventDefault();
props.search(searchValue);
setSearchValue("")
}
return (
<form className="search">
<input
value={searchValue}
onChange={handleSearchInputChanges}
type="text"
/>
<input onClick={callSearchFunction} type="submit" value="جستجو" />
</form>
);
}
export default Search;
بسیار خوب! همچنان که می بینید اولین هوک یعنی useState در کامپوننت Search استفاده شده است. همانطور که از نام آن پیداست این هوک به ما کمک می کند state را به کامپوننت تابعی Search اضافه کنیم. این هوک یک آرگومان ورودی می پذیرد که state اولیه است و خروجی آن آرایه ای شامل state جاری (معادل this.state در کامپوننت کلاسی) و تابعی برای آپدیت کردن state است (معادل this.setState).
در مثال بالا state جاری به عنوان مقدار فیلد جستجو قرار داده شده است. وقتی onChange اتفاق بیفتد تابع setSearchValue فرخوانی شده و باعث می شود searchValue که همان state ماست تغیر کند. با کلیک بر روی دکمه جستجو مقدار جاری state به تابع search که در کامپوننت App نوشته شده پاس داده شده و این تابع فراخوانی می شود.
نکته ای که در مورد useState در اینجا قابل تاکید است این است که تابع آپدیت کردن هر متغیر state از یک کلمه set به علاوه نام متغیر با اولین حرف بزرگ ساخته می شود مثل دو مثال زیر:
const [searchValue, setSearchValue] = useState("");
const [loading, setLoading] = useState(false);
در نهایت با در دست داشتن سه کامپوننت Header و Movie و Search محتوای App.js را مطابق کد زیر به روزرسانی کنید:
import React, { useState, useEffect } from "react";
import "../App.css";
import Header from "./Header";
import Movie from "./Movie";
import Search from "./Search";
const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b";
// apikey : شما از کلید خودتان استفاده کنید
const App = () => {
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => {
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
setMovies(jsonResponse.Search);
setLoading(false);
});
}, []);
const search = searchValue => {
setLoading(true);
setErrorMessage(null);
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
.then(response => response.json())
.then(jsonResponse => {
if (jsonResponse.Response === "True") {
setMovies(jsonResponse.Search);
setLoading(false);
} else {
setErrorMessage(jsonResponse.Error);
setLoading(false);
}
});
};
return (
<div className="App">
<Header text="اپ جستجوی فیلم با ری اکت هوک" />
<Search search={search} />
<p className="App-intro">با وارد کردن نام فیلم مورد علاقه خود آن را بیابید</p>
<div className="movies">
{loading && !errorMessage ? (
<span>در حال بارگذاری ...</span>
) : errorMessage ? (
<div className="errorMessage">{errorMessage}</div>
) : (
movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`} movie={movie} />
))
)}
</div>
</div>
);
};
export default App;
در کد بالا ما سه بار از useState استفاده کرده ایم و این یعنی در یک کامپوننت می توان از چندین useState استفاده کرد. اولی برای مدیریت state مربوط به بارگذاری (تا وقتی ریسپانس از API نیامده متن «در حال بارگذاری ...» را نشان بده؛ به محض لود شدن ریسپانس loading را false کن). دومی برای مدیریت آرایه فیلم ها که از سمت سرور گرفته می شود و سومی برای نمایش متن ارور در حالتی که اشکالی به وجود آمده است.
بعد از این سه useState ، نوبت به دومین هوک می رسد: یعنی useEffect. این هوک به زبان ساده به شما امکان میدهد کدهایی که ممکن است side effect یا اثر جانبی داشته باشند را در آن اجرا کنید. منظور از اثر جانبی عموما صدا کردن یک api از طریق fetch ، دستکاری DOM و چیزهایی از این دست است. به بیان ساده مطابق توضیح سایت رسمی ری اکت:
اگر شما با متدهای چرخه عمر یک کامپوننت کلاسی در ری اکت آشنا باشید، useEffect مانند ترکیبی از متدهای componentDidMount، componentDidUodate و componentWillUnmount است.
به همین دلیل useEffect بعد از اولین رندر شدن (componentDidMount) و نیز بعد از هر آپدیت در state یعنی (componenetDidUpdate) فراخوانی می شود. اگر قضیه یک مقدار برای شما گیج کننده شده حق دارید. چطور ممکن است که useEffect شبیه به componentDidMount باشد حال آنکه در هر آپدیت نیز فراخوانی می شود؟ اگر دقت کرده باشید useEffect دو ورودی می پذیرد: تابعی که قرار است در آن اجرا شود و پارامتر دوم که یک آرایه است. در این آرایه است که ما مشخص می کنیم که ری اکت باید تغییرات یا همان ساید افکت ها را عملی کند یا خیر...
در واقع بخش بالا و پایین در کد زیر یک معنا را دارند: (در بخش مربوط به هوک اگر count تغییر کرد کد درون useEffect را اجرا کن)
// برای کامپوننت کلاسی
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `clicked ${this.state.count} times`;
}
}
// وقتی که از این هوک استفاده می کنیم فقط وقتی متغیر ورودی دوم تغییر کند محتوای هوک اجرا می شود
useEffect(() => {
document.title = `clicked ${count} times`;
}, [count]);
در مثال پروژه ی سرچ فیلم مقداری نداریم که تغییر کند بنابراین آرایه خالی را فرستاده ایم تا به ری اکت بگوید کد ساید افکت یکبار انجام شود.
همانطور که ملاحظه می کنید در کامپوننت App سه تابع useState داریم که به نحوی به هم مربوط هستند و شاید بشود راهی پیدا کرد که بتوان آنها را به یک روشی با هم ترکیب کرد. خوشبختانه تیم ری اکت یک هوک برای کمک به این شرایط ایجاد کرده اند: useReducer
کد زیر را به جای کد قبلی در App.js جایگزین کنید:
import React, { useState, useEffect, useEffect } from "react";
import "../App.css";
import Header from "./Header";
import Movie from "./Movie";
import Search from "./Search";
const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b";
// apikey : شما از کلید خودتان استفاده کنید
const initialState = {
loading: true,
movies: [],
errorMessage: null
};
const reducer = (state, action) => {
switch (action.type) {
case "SEARCH_MOVIES_REQUEST":
return {
...state,
loading: true,
errorMessage: null
};
case "SEARCH_MOVIES_SUCCESS":
return {
...state,
loading: false,
movies: action.payload
};
case "SEARCH_MOVIES_FAILURE":
return {
...state,
loading: false,
errorMessage: action.error
};
default:
return state;
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
fetch(MOVIE_API_URL)
.then(response => response.json())
.then(jsonResponse => {
dispatch({
type: "SEARCH_MOVIES_SUCCESS",
payload: jsonResponse.Search
});
});
}, []);
const { movies, errorMessage, loading } = state;
const search = searchValue => {
dispatch({
type: "SEARCH_MOVIES_REQUEST"
});
fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`)
.then(response => response.json())
.then(jsonResponse => {
if (jsonResponse.Response === "True") {
dispatch({
type: "SEARCH_MOVIES_SUCCESS",
payload: jsonResponse.Search
});
} else {
dispatch({
type: "SEARCH_MOVIES_FAILURE",
error: jsonResponse.Error
});
}
});
};
return (
<div className="App">
<Header text="اپ جستجوی فیلم با ری اکت هوک" />
<Search search={search} />
<p className="App-intro">با وارد کردن نام فیلم مورد علاقه خود آن را بیابید</p>
<div className="movies">
{loading && !errorMessage ? (
<span>در حال بارگذاری ...</span>
) : errorMessage ? (
<div className="errorMessage">{errorMessage}</div>
) : (
movies.map((movie, index) => (
<Movie key={`${index}-${movie.Title}`} movie={movie} />
))
)}
</div>
</div>
);
};
export default App;
اگر همه چیز خوب پیش رفته باشد شما نباید تغییر در رفتار اپ قبل و بعد از استفاده از هوک useReducer ملاحظه کنید. اجازه دهید ببینیم این هوک یعنی useReducer چگونه کار می کند:
این هوک سه ورودی می پذیرد که در اینجا با دو تای اول آن کار داریم:
const [state, dispatch] = useReducer(reducer, initialState)
آرگومان اول یعنی reducer شبیه به reducer در ریداکس عمل می کند. یعنی تابعی است که state و action را به عنوان ورودی می گیرد و با توجه به type موجود در action یک آبجکت به عنوان state جدید بر می گرداند:
const reducer = (state, action) => {
switch (action.type) {
case "SEARCH_MOVIES_REQUEST":
return {
...state,
loading: true,
errorMessage: null
};
case "SEARCH_MOVIES_SUCCESS":
return {
...state,
loading: false,
movies: action.payload
};
case "SEARCH_MOVIES_FAILURE":
return {
...state,
loading: false,
errorMessage: action.error
};
default:
return state;
}
}
برای مثال اگر action ی که به reducer داده می شود SEARCH_MOVIES_REQUEST باشد، state به این شکل به روزرسانی می شود که loading مقدار true می گیرد یعنی فیلم ها در حال بارگذاری هستند و errorMessage مقدار null می گیرد چون هنوز پاسخی نیامده که اروری وجود داشته باشد!
دقت کنید که برای فراخوانی reducer و آپدیت کردن state از تابع dispatch مطابق کد زیر استفاده می کنیم:
dispatch({
type: "SEARCH_MOVIES_SUCCESS",
payload: jsonResponse.Search
}
تابع dispatch یک ورودی می گیرد و آن آبجکت action است. این آبجکت دو فیلد دارد: اولی نوع اکشن را مشخص می کند (مثلا «شروع درخواست»، «درخواست موفق» یا «درخواست ناموفق») و دومی payload یا داده ای که برای تغییر state لازم است را به reducer پاس می دهد. واضح است و نیاز به توضیح بیشتر ندارد که هریک از dispatch های موجود در App.js که نوشتیم چه کار می کند.
جمع بندی
پیاده سازی و معرفی پروژه سرچ فیلم به پایان رسید. امیدواریم با مفاهیم و کاربرد هوک ها آشنا شده باشید. شما می توانید هوک خودتان را بسازید اما معمولا همین سه هوکی که در اینجا معرفی شدند می توانند استخوان بندی یک پروژه ری اکت مبتنی بر کامپوننت های تابعی و هوک باشند.
موفق باشید.