چگونه با استفاده از React یک برنامه جستجوی تصویر بسازیم – یک آموزش عمیق
در این مقاله، گام به گام یک اپلیکیشن زیبای Unsplash Image Search را با صفحه بندی با استفاده از React می سازیم.
با ساخت این برنامه، یاد خواهید گرفت:
نحوه ساخت اپلیکیشن با استفاده از Unsplash API در React
نحوه برقراری تماس های API در سناریوهای مختلف
نحوه استفاده از قلاب useCallback
برای جلوگیری از ایجاد مجدد عملکرد
نحوه استفاده از ESLint برای رفع مشکلات برنامه
نحوه پیاده سازی صفحه بندی در React
و خیلی بیشتر...
آیا می خواهید نسخه ویدیویی این آموزش را تماشا کنید؟ می توانید ویدیوی زیر را مشاهده کنید:
راه اندازی اولیه پروژه
ما از Vite برای ایجاد پروژه ای استفاده خواهیم کرد که یک جایگزین محبوب برای create-react-app
است.
برای ایجاد پروژه vite دستور زیر را اجرا کنید:
npm create vite
پس از اجرا، از شما چند سوال پرسیده می شود.
برای نام پروژه، unsplash_image_search
را وارد کنید.
برای فریم ورک، React
و برای واریانت جاوا JavaScript
را انتخاب کنید:
پس از ایجاد پروژه، پروژه را در VS Code باز کنید و دستورات زیر را از ترمینال اجرا کنید:
cd unsplash_image_search npm install npm run dev
با رفتن به http://127.0.0.1:5173/ به برنامه دسترسی پیدا کنید.
صفحه پیش فرض برنامه را مطابق شکل زیر مشاهده خواهید کرد:
سپس فایل App.css
را حذف کنید و محتوای فایل App.jsx
را با محتوای زیر جایگزین کنید:
import React from 'react'; import './index.css'; const App = () => { return <div>Welcome to Unsplash Image Search</div>; }; export default App;
حالا فایل index.css
را باز کنید و محتویات این مخزن GitHub را به آن اضافه کنید.
بیایید بسته های B ootstrap و react-bootstrap npm را با اجرای دستور زیر نصب کنیم:
npm install bootstrap react-bootstrap
فایل main.jsx
را باز کنید و خط کد زیر را در خط اول اضافه کنید تا فایل CSS پایه بوت استرپ را اضافه کنید:
import 'bootstrap/dist/css/bootstrap.min.css';
فایل main.jsx
کامل به شکل زیر خواهد بود:
import 'bootstrap/dist/css/bootstrap.min.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> );
اکنون با اجرای دستور npm run dev
برنامه را ریستارت کنید.
پیام خوشامدگویی را مطابق شکل زیر روی صفحه نمایش خواهید دید:
نحوه گفت ن ورودی جستجو
اکنون محتوای فایل App.jsx
را با محتوای زیر جایگزین کنید:
import React from 'react'; import { Form } from 'react-bootstrap'; import './index.css'; const App = () => { return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form> <Form.Control type='search' placeholder='Type something to search...' className='search-input' /> </Form> </div> </div> ); }; export default App;
در اینجا، عنوان Image search
را در یک کلاس container
، که یک کلاس Bootstrap است، نمایش میدهیم تا مقداری حاشیه به سمت چپ و راست صفحه اضافه کنیم.
سپس یک فرم f را با یک نوع search
اضافه کردیم.
اگر برنامه را تحلیل کنید، صفحه زیر را مشاهده خواهید کرد:
حال باید مقدار وارد شده توسط کاربر را در جایی از کامپوننت ذخیره کنیم.
از آنجایی که ما فقط یک فیلد ورودی در صفحه خواهیم داشت، به جای قلاب useState
از قلاب useRef استفاده می کنیم.
استفاده از قلاب useRef
وقتی مقدار آن تغییر میکند، مؤلفه را دوباره رندر نمیکند، که برای بهبود عملکرد خوب است. از طرف دیگر، تغییر حالت کامپوننت را دوباره رندر می کند، پس همه مولفه های فرزند نیز دوباره رندر می شوند.
در داخل فایل App.jsx
، قلاب useRef
را مطابق شکل زیر اعلام کنید:
const searchInput = useRef(null);
فراموش نکنید که import for useRef
hook را در بالای فایل اضافه کنید:
import React, { useRef } from 'react';
همچنین، یک ref
prop برای ورودی جستجو اضافه کنید، مانند این:
<Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} />
فایل App.jsx
کامل شما به شکل زیر خواهد بود:
import React, { useRef } from 'react'; import { Form } from 'react-bootstrap'; import './index.css'; const App = () => { const searchInput = useRef(null); return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form> <Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} /> </Form> </div> </div> ); }; export default App;
نحوه رسیدگی به اقدام ارسال فرم
وقتی هر عبارت جستجو را در کادر جستجو وارد می کنیم و کلید enter را فشار می دهیم، قصد داریم قابلیت جستجو را اضافه کنیم.
برای انجام این کار، یک onSubmit
handler را به تگ Form
اضافه کنید و یک متد handleSearch
ایجاد کنید. و آن را به شکل زیر به onSubmit
اختصاص دهید:
import React, { useRef } from 'react'; import { Form } from 'react-bootstrap'; import './index.css'; const App = () => { const searchInput = useRef(null); const handleSearch = (event) => { event.preventDefault(); console.log('submitted'); }; return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form onSubmit={handleSearch}> <Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} /> </Form> </div> </div> ); }; export default App;
در اینجا، <Form onSubmit={handleSearch}>
اضافه کردهایم و در متد handleSearch
از متد event.preventDefault
استفاده کردهایم.
هنگامی که فرم با فشار دادن کلید enter در کادر جستجو ارسال شد، صفحه بازخوانی نمی شود و متن ارسالی مطابق شکل زیر در کنسول نمایش داده می شود:
اکنون به جای چاپ "submitted" می توانیم مقدار وارد شده توسط کاربر را با استفاده از searchInput.current.value
چاپ کنیم.
در اینجا searchInput
ref
است و searchInput.current
ورودی جعبه جستجوی واقعی خواهد بود. همچنین استفاده از searchInput.current.value
مقدار واقعی وارد شده توسط کاربر را به دست می دهد.
پس ، متد handleSearch
را با کد زیر جایگزین کنید:
const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); };
و اکنون مقدار وارد شده را در کنسول خواهید دید:
نحوه اضافه کردن گزینه های جستجوی سریع
اکنون، اجازه دهید دکمههای عمل را با دستهای از filters
برای جستجوی سریع درست در زیر search-section
اضافه کنیم:
<div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> ... </div> <div className='filters'> <div>Nature</div> <div>Birds</div> <div>Cats</div> <div>Shoes</div> </div> </div>
اکنون، برنامه به شکل زیر خواهد بود:
وقتی روی هر یک از دکمه های نمایش داده شده کلیک می کنیم، می توانیم مقدار دکمه کلیک شده را در کادر جستجوی ورودی نمایش دهیم، پس می توانیم از آن برای جستجوی تصاویر استفاده کنیم.
filters
div را به کد زیر تغییر دهید:
<div className='filters'> <div onClick={() => handleSelection('nature')}>Nature</div> <div onClick={() => handleSelection('birds')}>Birds</div> <div onClick={() => handleSelection('cats')}>Cats</div> <div onClick={() => handleSelection('shoes')}>Shoes</div> </div>
در کد بالا، وقتی روی هر گزینه ای کلیک می کنید، گزینه انتخاب شده را به روش handleSelection
منتقل می کنیم.
اکنون، یک متد جدید handleSelection
را در داخل مؤلفه App
اضافه کنید، همانطور که در زیر نشان داده شده است:
const handleSelection = (selection) => { searchInput.current.value = selection; };
فایل App.jsx
کامل شما به شکل زیر خواهد بود:
import React, { useRef } from 'react'; import { Form } from 'react-bootstrap'; import './index.css'; const App = () => { const searchInput = useRef(null); const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); }; const handleSelection = (selection) => { searchInput.current.value = selection; }; return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form onSubmit={handleSearch}> <Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} /> </Form> </div> <div className='filters'> <div onClick={() => handleSelection('nature')}>Nature</div> <div onClick={() => handleSelection('birds')}>Birds</div> <div onClick={() => handleSelection('cats')}>Cats</div> <div onClick={() => handleSelection('shoes')}>Shoes</div> </div> </div> ); }; export default App;
نحوه دسترسی به Unsplash API
اکنون برای پیاده سازی جستجوی تصویر، باید کلید API را از وب سایت Unsplash دریافت کنیم.
به این URL بروید و روی دکمه "ثبت نام به عنوان توسعه دهنده" که در گوشه سمت راست بالای صفحه نمایش داده شده است کلیک کنید. با وارد کردن تمام جزئیات لازم حساب کاربری خود را ایجاد کنید.
پس از ثبت نام، مطابق شکل زیر به این صفحه هدایت خواهید شد:
بر روی دکمه New Application
کلیک کنید. در صفحه بعدی:
تمام چک باکس ها را علامت بزنید و روی دکمه Accept Terms
کلیک کنید
مقادیر نام و Description
Application name
را وارد کنید و روی دکمه Create application
کلیک کنید
کمی به پایین اسکرول کنید و Access Key
را که روی صفحه نمایش داده می شود کپی کنید:
در مرحله بعد، یک فایل .env
جدید در پروژه خود ایجاد کنید و یک متغیر محیطی جدید با نام VITE_API_KEY
اضافه کنید. همچنین مقدار کپی شده کلید API را به آن اختصاص دهید:
VITE_API_KEY=A4UiJ5OIwL_4ccbCAE1ZXw3EgoNRotMbdNe12qtKHzM
مطمئن شوید که نام متغیر را با VITE_
شروع کرده اید تا در برنامه قابل دسترسی باشد.
ساختار پوشه برنامه شما به شکل زیر خواهد بود:
همچنین، حتماً .env
در فایل .gitignore
اضافه کنید تا زمانی که تغییرات به GitHub منتقل میشوند، فایل به GitHub فرستاده نشود.
اکنون به Unsplash Documentation رفته و روی قسمت Search photos by keyword
کلیک کنید. و URL پایه API زیر را کپی کنید: https://api.unsplash.com/search/photos
.
اکنون، فایل App.jsx
را باز کنید و URL کپی شده را بعد از تمام دستورات وارد کردن به عنوان API_URL
جایگذاری کنید، مانند این:
const API_URL = 'https://api.unsplash.com/search/photos';
با توجه به مستندات، API عکسهای جستجو با نشانی اینترنتی بالا، query
، page
و per_page
به عنوان پارامترهای جستجو میپذیرد. فقط به این توجه کنید، زیرا به زودی از آن استفاده خواهیم کرد.
نحوه برقراری تماس API با Unsplash API
برای برقراری تماس API، ابتدا کتابخانه axios
npm را با اجرای دستور زیر از پوشه پروژه نصب می کنیم:
npm install axios
پس از نصب، با اجرای دستور npm run dev
برنامه را دوباره راه اندازی کنید.
سپس، یک ثابت جدید درست زیر ثابت API_URL
اعلام کنید:
const IMAGES_PER_PAGE = 20;
در اینجا، زمانی که صفحه بندی را پیاده سازی می کنیم، مشخص می کنیم که ۲۰
تصویر در هر صفحه نمایش داده شود. می توانید آن را به هر مقداری که می خواهید تغییر دهید.
یک تابع fetchImages
جدید در داخل کامپوننت App
اضافه کنید مانند این:
const fetchImages = async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=1&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); } catch (error) { console.log(error); } };
در اینجا، ما یک تابع fetchImages
را تعریف کردهایم که async
اعلام شده است، پس میتوانیم await
در داخل آن استفاده کنیم.
اگر از وعدهها و همگامسازی/انتظار آگاه نیستید، به شدت توصیه میکنم این مقاله را تحلیل کنید.
سپس، در داخل تابع fetchImages
، با استفاده از axios به URL که در ثابت API_URL
ذخیره کردهایم، یک فراخوانی GET API برقرار میکنیم: https://api.unsplash.com/search/photos
.
برای URL API، ما پارامترهای پرس و جوی زیر را با استفاده از نحو تحت اللفظی الگو ارسال می کنیم:
query
با مقدار کاربر وارد شده یا مقدار گزینه جستجوی سریع
page
با مقدار ۱
برای دریافت داده های صفحه اول
per_page
با مقدار ۲۰
که در ثابت IMAGES_PER_PAGE
تعریف شده است
client_id
با مقدار کلید API از فایل .env
.
همانطور که از Vite استفاده می کنیم، برای دسترسی به متغیرهای محیطی از فایل .env
، باید از import.meta.env.VITE_API_KEY
استفاده کنیم.
در اینجا، VITE_API_KEY
متغیر محیطی است که در فایل .env
اعلام کردیم.
همچنین کتابخانه axios
را در بالای فایل به صورت زیر وارد کنید:
import axios from axios;
فایل آپدیت شده App.jsx
به شکل زیر خواهد بود:
import axios from 'axios'; import React, { useRef } from 'react'; import { Form } from 'react-bootstrap'; import './index.css'; const API_URL = 'https://api.unsplash.com/search/photos'; const IMAGES_PER_PAGE = 20; const App = () => { const searchInput = useRef(null); const fetchImages = async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=1&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); } catch (error) { console.log(error); } }; const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); }; const handleSelection = (selection) => { searchInput.current.value = selection; fetchImages(); }; return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form onSubmit={handleSearch}> <Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} /> </Form> </div> <div className='filters'> <div onClick={() => handleSelection('nature')}>Nature</div> <div onClick={() => handleSelection('birds')}>Birds</div> <div onClick={() => handleSelection('cats')}>Cats</div> <div onClick={() => handleSelection('shoes')}>Shoes</div> </div> </div> ); }; export default App;
اگر برنامه را تحلیل کنید، خواهید دید که با هر کلیک روی گزینه جستجوی سریع، فراخوانی API با Unsplash API برقرار می شود و ما داده های گزینه انتخاب شده را دریافت می کنیم.
برای برقراری تماس API هنگامی که متن جستجو را وارد می کنیم و کلید enter را فشار می دهیم، باید تابع fetchImages
را از تابع handleSearch
نیز فراخوانی کنیم.
برای انجام این کار، مطابق شکل زیر یک فراخوانی به تابع fetchImages
در داخل تابع handleSearch
اضافه کنید:
const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); fetchImages(); };
اکنون، هنگامی که متن جستجو را وارد می کنیم و کلید Enter را فشار می دهیم، می توانید تماس API انجام شده در تب شبکه را مشاهده کنید.
نحوه ذخیره داده های API با استفاده از حالت
حالا بیایید تصاویری را که از API می آیند روی صفحه نمایش دهیم.
برای نمایش آنها بر روی صفحه، ابتدا باید داده های دریافتی از API را ذخیره کنیم.
اگر ساختار پاسخ API را مشاهده کنید، مانند شکل زیر خواهید دید:
پس ، دو حالت را در فایل App.jsx
اعلام کنید: یکی برای ذخیره تصاویر پاسخ که در ویژگی results
می آیند، و دیگری برای ذخیره total_pages
تا بتوانیم صفحه بندی را پیاده سازی کنیم.
const App = () => { const searchInput = useRef(null); const [images, setImages] = useState([]); const [totalPages, setTotalPages] = useState(0); .... }
و تابع fetchImages
را برای ذخیره data.results
با استفاده از setImages
و کل صفحات با استفاده از تابع setTotalPages
به روز کنید:
const fetchImages = async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=1&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } catch (error) { console.log(error); } };
نحوه نمایش تصاویر روی صفحه
حال بیایید تصاویری را که در متغیر وضعیت images
ذخیره کرده ایم نمایش دهیم.
اگر پاسخ تصویر فردی API را گسترش دهید، میتوانید آپشن های id
، alt_description
، urls
مشاهده کنید که میتوانیم از آنها برای نمایش تصاویر جداگانه استفاده کنیم.
پس ، درست بعد از filters
، یک div دیگری برای نمایش تصاویری مانند این اضافه کنید:
<div className='filters'> ... </div> <div className='images'> {images.map((image) => { return ( <img key={image.id} src={image.urls.small} alt={image.alt_description} className='image' /> ); })} </div>
در اینجا، ما نسخه small
تصویر را از ویژگی urls
هر تصویر نشان می دهیم.
ما می توانیم کد بالا را بیشتر ساده کنیم. در روش map
آرایه، به جای استفاده از یک براکت فرفری با کلمه کلیدی return
، میتوانیم آن را به صورت زیر بازنویسی کنیم:
<div className='filters'> ... </div> <div className='images'> {images.map((image) => ( <img key={image.id} src={image.urls.small} alt={image.alt_description} className='image' /> ))} </div>
در اینجا، ما به طور ضمنی JSX را از روش map
آرایه با گفت ن یک براکت گرد در اطراف JSX برمی گردانیم.
حال اگر هر متنی را جستجو کنید، تصاویر را به درستی نمایش می دهید.
اکنون دکمه های قبلی و بعدی را اضافه می کنیم تا مجموعه های مختلف تصاویر را مشاهده کنیم.
پس ، ابتدا یک حالت جدید در کامپوننت App
مانند شکل زیر اعلام کنید:
const [page, setPage] = useState(1);
در داخل تابع fetchImages
، page=1
به page=${page}
تغییر دهید تا زمانی که مقدار page
را تغییر میدهیم، تصاویر page
انتخابی بارگیری میشوند.
همانطور که در زیر نشان داده شده است، یک div جدید با یک کلاس از buttons
درست در زیر div images
اضافه کنید:
<div className='images'> ... </div> <div className='buttons'> {page > 1 && <Button>Previous</Button>} {page < totalPages && <Button>Next</Button>} </div>
در کد بالا فقط در صورتی دکمه Previous
را نشان می دهیم که مقدار page
بزرگتر از ۱
باشد، یعنی برای صفحه اول، دکمه Previous
را نخواهیم دید.
و اگر مقدار فعلی page
کمتر از totalPages
باشد، فقط دکمه Next
را نشان می دهیم. یعنی برای صفحه آخر، دکمه Next
را نخواهیم دید.
اگر به خاطر داشته باشید، ما قبلاً با فراخوانی تابع setTotalPages
، مقدار totalPages
را در تابع fetchImages
تنظیم کردهایم و از آن در بالا برای مخفی کردن دکمه Next
استفاده میکنیم.
همچنین، فراموش نکنید که وارد کردن کامپوننت Button
از react-bootstrap
داخل کامپوننت App
اضافه کنید:
import { Button } from 'react-bootstrap';
حالا وقتی روی دکمه Previous
کلیک می کنیم، باید مقدار متغیر page
state را decrement
. و وقتی روی دکمه Next
کلیک می کنیم باید مقدار متغیر page
state را increment
.
پس ، بیایید یک کنترل کننده onClick
برای هر دوی این دکمه ها مانند شکل زیر اضافه کنیم:
<div className='buttons'> {page > 1 && ( <Button onClick={() => setPage(page - 1)}>Previous</Button> )} {page < totalPages && ( <Button onClick={() => setPage(page + 1)}>Next</Button> )} </div>
بیایید مقدار متغیر page
state را وارد کنسول کنیم تا بتوانیم مقدار به روز شدن آن را ببینیم.
پس از روش handleSelection
، console.log را به این صورت اضافه کنید:
console.log('page', page);
همانطور که در بالا می بینید، در ابتدا برای صفحه اول، دکمه Previous
را نمی بینیم.
و وقتی روی دکمه Next
کلیک می کنیم دکمه های Previous
و Next
را می بینیم و مقدار page
نیز همانطور که در کنسول مشاهده می کنید ۱
افزایش می یابد.
پس ، با هر کلیک روی دکمه Next
، مقدار page
1
افزایش می یابد. و با هر کلیک روی دکمه Previous
، مقدار page
1
کاهش می یابد.
و هنگامی که به صفحه اول باز می گردیم، دکمه Previous
دوباره پنهان می شود که مطابق انتظار است.
همانطور که در بالا متوجه شده اید، با کلیک روی دکمه های Previous
و Next
، مقدار صفحه تغییر می کند، اما وقتی روی آن دکمه ها کلیک می کنیم مجموعه جدیدی از تصاویر بارگذاری نمی شوند.
این به این دلیل است که وقتی مقدار صفحه تغییر می کند، دوباره با یک مقدار صفحه به روز شده تماس API برقرار نمی کنیم.
پس بیایید همین کار را بکنیم.
یک قلاب useEffect
را در کامپوننت App
به این صورت اضافه کنید:
useEffect(() => { fetchImages(); }, [page]);
اکنون، هر بار که روی دکمه Previous
یا Next
کلیک می کنیم، مقدار page
تغییر می کند، پس قلاب useEffect
بالا اجرا می شود، جایی که ما تابع fetchImages
را برای بارگذاری مجموعه بعدی تصاویر فراخوانی می کنیم.
اکنون، اگر برنامه را تحلیل کنید، تصاویر به درستی بارگذاری شده اند.
همانطور که در بالا می بینید، وقتی روی دکمه Previous
یا Next
کلیک می کنیم، تصاویر را به درستی بارگذاری می کنیم.
اما یک مسئله کوچک وجود دارد.
اگر در صفحه اول یا آخر نباشیم، دکمه های Previous
و Next
را می بینیم و وقتی قصد داریم عبارت دیگری را جستجو کنیم یا روی گزینه های جستجوی سریع کلیک می کنیم، همچنان دکمه Previous
را می بینیم.
در حالت ایده آل، زمانی که عبارت دیگری را جستجو می کنیم یا روی گزینه جستجوی سریع دیگری کلیک می کنیم، باید از صفحه اول شروع کنیم، پس فقط دکمه Next
باید قابل مشاهده باشد. اما در حال حاضر هر دو دکمه Previous
و Next
همانطور که در زیر می بینید قابل مشاهده هستند:
برای رفع این مشکل، زمانی که عبارت دیگری را جستجو می کنیم یا روی گزینه جستجوی سریع دیگری کلیک می کنیم، باید مقدار حالت page
را بازنشانی کنیم.
پس در داخل متدهای handleSearch
و handleSelection
، تابع setPage
را با مقدار ۱
مانند این فراخوانی کنید:
const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); fetchImages(); setPage(1); }; const handleSelection = (selection) => { searchInput.current.value = selection; fetchImages(); setPage(1); };
همانطور که می بینید، ما فراخوانی های تابع fetchImages
و setPage
را در هر دوی این روش ها تکرار می کنیم.
پس ، بیایید یک تابع دیگر با نام resetSearch
ایجاد کنیم و فراخوانی های تابع fetchImages
و setPage
را داخل آن منتقل کنیم. بیایید آن تابع را از متدهای handleSearch
و handleSelection
مانند شکل زیر فراخوانی کنیم:
const resetSearch = () => { setPage(1); fetchImages(); }; const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); resetSearch(); }; const handleSelection = (selection) => { searchInput.current.value = selection; resetSearch(); };
اکنون، اگر برنامه را تحلیل کنید، خواهید دید که ما همیشه با کلیک بر روی گزینه جستجوی سریع یا وارد کردن هر عبارت جستجویی که مطابق انتظار است، نتیجه صحیح صفحه اول نمایش داده می شود.
کل فایل App.jsx
شما به شکل زیر خواهد بود:
import axios from 'axios'; import { useEffect, useRef, useState } from 'react'; import { Button, Form } from 'react-bootstrap'; import './index.css'; const API_URL = 'https://api.unsplash.com/search/photos'; const IMAGES_PER_PAGE = 20; const App = () => { const searchInput = useRef(null); const [images, setImages] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(0); useEffect(() => { fetchImages(); }, [page]); const fetchImages = async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } catch (error) { console.log(error); } }; const resetSearch = () => { setPage(1); fetchImages(); }; const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); resetSearch(); }; const handleSelection = (selection) => { searchInput.current.value = selection; resetSearch(); }; console.log('page', page); return ( <div className='container'> <h1 className='title'>Image Search</h1> <div className='search-section'> <Form onSubmit={handleSearch}> <Form.Control type='search' placeholder='Type something to search...' className='search-input' ref={searchInput} /> </Form> </div> <div className='filters'> <div onClick={() => handleSelection('nature')}>Nature</div> <div onClick={() => handleSelection('birds')}>Birds</div> <div onClick={() => handleSelection('cats')}>Cats</div> <div onClick={() => handleSelection('shoes')}>Shoes</div> </div> <div className='images'> {images.map((image) => ( <img key={image.id} src={image.urls.small} alt={image.alt_description} className='image' /> ))} </div> <div className='buttons'> {page > 1 && ( <Button onClick={() => setPage(page - 1)}>Previous</Button> )} {page < totalPages && ( <Button onClick={() => setPage(page + 1)}>Next</Button> )} </div> </div> ); }; export default App;
نحوه پیدا کردن اشکالات با استفاده از ESLint
هنگام کار بر روی یک برنامه React، همیشه باید پسوند ESLint VS Code را فعال کنید.
با این کار مطمئن می شوید که کد شما صحیح است و در آینده هیچ نتیجه غیرمنتظره ای ایجاد نخواهد کرد.
بر اساس پیکربندی ESLint تعریف شده در فایل .eslientrc
، پیشنهادات مفیدی برای بهبود کد خود دریافت خواهید کرد.
پس ، پنل VS Code Extensions خود را باز کنید و پسوند ESLint را مطابق شکل زیر نصب کنید:
پس از نصب افزونه، اگر فایل App.jsx
را تحلیل کنید، بلافاصله یک خط زرد رنگ برای وابستگی page
قلاب useEffect
مشاهده خواهید کرد. اگر ماوس را روی آن قرار دهید، هشداری را مانند شکل زیر مشاهده خواهید کرد:
همانطور که اخطار نشان می دهد، باید یک وابستگی fetchImages
را در آرایه وابستگی اضافه کنیم.
ما یک اخطار دریافت می کنیم زیرا در کامپوننت عملکردی، در هر رندر مجدد کامپوننت، همه توابع اعلام شده دوباره ایجاد می شوند پس مرجع آنها تغییر می کند.
پس ، اگر از هر متغیر یا تابع بیرونی در داخل قلاب useEffect
استفاده میکنیم، باید به این نکته اشاره کنیم که در وابستگیها، پس هر زمان که وابستگی تغییر کرد، useEffect
دوباره اجرا میشود.
برای رفع این مشکل میتوانید روی لینک رفع سریع کلیک کنید و گزینه «بهروزرسانی وابستگیها» را مطابق شکل زیر انتخاب کنید:
تمام وابستگی های از دست رفته به طور خودکار در آرایه وابستگی اضافه می شوند.
همچنین در صورت تمایل می توانید وابستگی را به صورت دستی اضافه کنید.
با این حال، با این تغییر، یک هشدار زرد جدید برای تابع fetchImages
مانند شکل زیر مشاهده خواهید کرد:
همانطور که قبلاً گفتم، در هر رندر مجدد کامپوننت، تابع fetchImages
دوباره ایجاد می شود و وقتی تغییر کرد، ما دوباره تابع fetchImages
را همانطور که در وابستگی اضافه شده است فراخوانی می کنیم.
برای جلوگیری از آن، باید تابع fetchImages
را مانند شکل زیر در داخل قلاب useCallback قرار دهیم:
const fetchImages = useCallback(async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } catch (error) { console.log(error); } }, [page]);
در کد بالا، page
بهعنوان یک وابستگی ارسال میکنیم، زیرا page
یک متغیر خارجی است که با کلیک روی دکمههای Previous
یا Next
یا جستجوی هر عبارت جدید، مقدار آن ممکن است در آینده تغییر کند.
اگر متغیرهای تغییر دهنده در داخل useEffect
یا useCallback
یا useMemo
hook استفاده میشوند، باید آنها را در فهرست وابستگیها اضافه کنیم.
اکنون دیگر هیچ هشداری در مؤلفه App
نخواهید دید.
با این حال، اگر کنسول مرورگر را تحلیل کنید، با خطا مواجه خواهید شد و چیزی در رابط کاربری نمایش داده نمی شود زیرا برنامه از کار افتاده است.
خطاهایی دریافت میکنیم زیرا تابع fetchImages
را با استفاده از نحو عبارت تابع اعلام کردهایم، و توابع اعلامشده با استفاده از نحو عبارت تابع را نمیتوان قبل از تعریف آنها فراخوانی کرد.
اختصاص یک تابع به یک متغیر، آن را به یک عبارت تابع تبدیل می کند.
همانطور که در تصویر زیر می بینید، ما تابع fetchImages
را در خط شماره ۱۶ فراخوانی می کنیم و تابع را در خط شماره ۱۹ اعلام می کنیم و توابع اعلام شده با استفاده از نحو عبارت تابع قبل از اعلان قابل دسترسی نیستند.
برای رفع این مشکل، باید تابع را قبل از فراخوانی آن اعلام کنیم. پس ، تابع fetchImages
را قبل از قلاب useEffect حرکت دهید تا مشکل برطرف شود.
کامپوننت App
شما به شکل زیر خواهد بود:
const App = () => { const searchInput = useRef(null); const [images, setImages] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const fetchImages = useCallback(async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } catch (error) { console.log(error); } }, [page]); useEffect(() => { fetchImages(); }, [fetchImages, page]); const resetSearch = () => { setPage(1); fetchImages(); }; ... }
حال اگر اپلیکیشن را تحلیل کنید هیچ خطایی وجود نخواهد داشت و برنامه طبق انتظار عمل می کند.
بهبود کد
در حال حاضر، وقتی کاربر یک عبارت جستجو را وارد می کند، هیچ اعتبارسنجی در برنامه فعلی اضافه نکرده ایم.
وقتی صفحه بارگیری می شود، و وقتی هیچ متنی را وارد نمی کنیم و مستقیماً کلید enter را در کادر جستجوی ورودی فشار می دهیم، یک تماس API برقرار می کنیم که خوب نیست.
برای رفع این مشکل، قبل از برقراری تماس API، ابتدا باید تحلیل کنیم که searchInput.current.value
خالی نیست و سپس فقط تماس API را انجام دهیم.
تابع fetchImages
از این کد تغییر دهید:
const fetchImages = useCallback(async () => { try { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } catch (error) { console.log(error); } }, [page]);
به کد زیر:
const fetchImages = useCallback(async () => { try { if (searchInput.current.value) { const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); console.log('data', data); setImages(data.results); setTotalPages(data.total_pages); } } catch (error) { console.log(error); } }, [page]);
همانطور که در بالا مشاهده می کنید، ابتدا در بارگذاری صفحه و بدون وارد کردن مقدار، اگر کلید enter را فشار دهیم، هیچ تماس API برقرار نمی شود.
فقط وقتی چیزی را تایپ می کنیم و enter را فشار می دهیم، فراخوانی API برقرار می شود که پیشرفت خوبی برای برنامه است.
همانطور که یک قلاب useCallback
برای تابع fetchImages
اضافه کردهایم که وابستگی page
دارد، دیگر نیازی به وابستگی page
اضافی برای قلاب useEffect
نداریم.
پس کد زیر را تغییر دهید:
useEffect(() => { fetchImages(); }, [fetchImages, page]);
useEffect(() => { fetchImages(); }, [fetchImages]);
و برنامه مانند قبل بدون هیچ مشکلی کار خواهد کرد.
نحوه نمایش نشانه بارگذاری
همانطور که ممکن است در تصویر قبلی متوجه شده باشید، زمانی که ما متن hello
را جستجو کردیم، نتایج بلافاصله نمایش داده نشد.
از آنجایی که هنگام جستجوی چیزی در حال برقراری تماس با API هستیم، بسته به سرعت شبکه، دریافت دادهها از API ممکن است کمی طول بکشد.
پس در حالی که تماس API هنوز ادامه دارد، میتوانیم یک پیام بارگیری را نمایش دهیم و پس از دریافت پاسخ از API، تصاویر را نمایش خواهیم داد.
برای دستیابی به آن، یک حالت بارگذاری جدید در مؤلفه App
با مقدار اولیه false
اعلام کنید:
const [loading, setLoading] = useState(false);
و حالا تابع fetchImages
را به کد زیر تغییر دهید:
const fetchImages = useCallback(async () => { try { if (searchInput.current.value) { setErrorMsg(''); setLoading(true); const { data } = await axios.get( `${API_URL}?query=${ searchInput.current.value }&page=${page}&per_page=${IMAGES_PER_PAGE}&client_id=${ import.meta.env.VITE_API_KEY }` ); setImages(data.results); setTotalPages(data.total_pages); setLoading(false); } } catch (error) { setErrorMsg('Error fetching images. Try again later.'); console.log(error); setLoading(false); } }, [page]);
همانطور که در بالا می بینید، ما setLoading(true)
قبل از تماس API و setLoading(false)
بعد از تماس API فراخوانی می کنیم.
توجه داشته باشید که ما `setLoading(false)
در داخل بلوک catch نیز فراخوانی می کنیم.
پس ، حتی اگر API موفق یا ناموفق باشد، وضعیت loading
روی false
تنظیم میکنیم تا پیام بارگیری را همیشه نبینیم.
اکنون برای نمایش پیام بارگذاری کد زیر را تغییر دهید:
<div className='images'> {images.map((image) => ( <img key={image.id} src={image.urls.small} alt={image.alt_description} className='image' /> ))} </div> <div className='buttons'> {page > 1 && ( <Button onClick={() => setPage(page - 1)}>Previous</Button> )} {page < totalPages && ( <Button onClick={() => setPage(page + 1)}>Next</Button> )} </div>
{loading ? ( <p className='loading'>Loading...</p> ) : ( <> <div className='images'> {images.map((image) => ( <img key={image.id} src={image.urls.small} alt={image.alt_description} className='image' /> ))} </div> <div className='buttons'> {page > 1 && ( <Button onClick={() => setPage(page - 1)}>Previous</Button> )} {page < totalPages && ( <Button onClick={() => setPage(page + 1)}>Next</Button> )} </div> </> )}
در کد بالا، اگر بارگذاری درست باشد، پیام بارگذاری را نمایش می دهیم. در غیر این صورت، ما تصاویری را که از API می آیند نمایش می دهیم.
اگر برنامه را تحلیل کنید، خواهید دید که نشانگر بارگذاری به درستی نمایش داده می شود.
با تشکر برای خواندن
برای این آموزش تمام شد. امیدوارم چیزهای زیادی از آن یاد گرفته باشید.
شما می توانید کد منبع کامل این برنامه را در این مخزن بیابید.
Want to watch the video version of this tutorial? You can check out this video.
If you want to master JavaScript, ES6+, React, and Node.js with easy-to-understand content, check out my YouTube channel . اشتراک را فراموش نکنید.
Want to stay up to date with regular content on JavaScript, React, and Node.js? Follow me on LinkedIn .
ارسال نظر