سایت خبرکاو

جستجوگر هوشمند اخبار و مطالب فناوری

چگونه با استفاده از React یک برنامه جستجوی تصویر بسازیم – یک آموزش عمیق

در این مقاله، گام به گام یک اپلیکیشن زیبای Unsplash Image Search را با صفحه بندی با استفاده از React می سازیم. با ساخت این برنامه، یاد خواهید گرفت: نحوه ساخت اپلیکیشن با استفاده از Unsplash API در React نحوه برقراری تماس های API در سناریوهای مختلف نحوه استفاده از قلاب useCallback برای جلوگیری از ایجاد مجدد عملکرد نحوه استفاده از ESLint برای رفع مشکلات برنامه نحوه پیاده سازی صفحه بندی در 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 را انتخاب کنید:

2_ایجاد_پروژه
ایجاد پروژه با استفاده از Vite

پس از ایجاد پروژه، پروژه را در VS Code باز کنید و دستورات زیر را از ترمینال اجرا کنید:

 cd unsplash_image_search npm install npm run dev

با رفتن به http://127.0.0.1:5173/ به برنامه دسترسی پیدا کنید.

3_پروژه_شروع شد
برنامه شروع شد

صفحه پیش فرض برنامه را مطابق شکل زیر مشاهده خواهید کرد:

1_app_screen
صفحه اولیه

سپس فایل 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 برنامه را ریستارت کنید.

پیام خوشامدگویی را مطابق شکل زیر روی صفحه نمایش خواهید دید:

4_صفحه_اولیه
صفحه خوش آمدید

نحوه گفت ن ورودی جستجو

اکنون محتوای فایل 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 اضافه کردیم.

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

5_جستجو
رابط کاربری جستجوی اولیه

حال باید مقدار وارد شده توسط کاربر را در جایی از کامپوننت ذخیره کنیم.

از آنجایی که ما فقط یک فیلد ورودی در صفحه خواهیم داشت، به جای قلاب 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 در کادر جستجو ارسال شد، صفحه بازخوانی نمی شود و متن ارسالی مطابق شکل زیر در کنسول نمایش داده می شود:

6_ارائه شد
اقدام ارسال فرم

اکنون به جای چاپ "submitted" می توانیم مقدار وارد شده توسط کاربر را با استفاده از searchInput.current.value چاپ کنیم.

در اینجا searchInput ref است و searchInput.current ورودی جعبه جستجوی واقعی خواهد بود. همچنین استفاده از searchInput.current.value مقدار واقعی وارد شده توسط کاربر را به دست می دهد.

پس ، متد handleSearch را با کد زیر جایگزین کنید:

 const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); };

و اکنون مقدار وارد شده را در کنسول خواهید دید:

7_ترم جستجو
نمایش مقدار عبارت جستجوی وارد شده در کنسول

نحوه اضافه کردن گزینه های جستجوی سریع

اکنون، اجازه دهید دکمه‌های عمل را با دسته‌ای از 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>

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

8_buttons_added
گزینه های جستجوی سریع اضافه شد

وقتی روی هر یک از دکمه های نمایش داده شده کلیک می کنیم، می توانیم مقدار دکمه کلیک شده را در کادر جستجوی ورودی نمایش دهیم، پس می توانیم از آن برای جستجوی تصاویر استفاده کنیم.

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;
9_انتخاب
نمایش گزینه انتخاب در جعبه جستجو

نحوه دسترسی به Unsplash API

اکنون برای پیاده سازی جستجوی تصویر، باید کلید API را از وب سایت Unsplash دریافت کنیم.

به این URL بروید و روی دکمه "ثبت نام به عنوان توسعه دهنده" که در گوشه سمت راست بالای صفحه نمایش داده شده است کلیک کنید. با وارد کردن تمام جزئیات لازم حساب کاربری خود را ایجاد کنید.

10_register_as_developer

پس از ثبت نام، مطابق شکل زیر به این صفحه هدایت خواهید شد:

11_applications_page
ثبت برنامه جدید با Unsplash API

بر روی دکمه New Application کلیک کنید. در صفحه بعدی:

تمام چک باکس ها را علامت بزنید و روی دکمه Accept Terms کلیک کنید

مقادیر نام و Description Application name را وارد کنید و روی دکمه Create application کلیک کنید

13_create_application
ایجاد برنامه جدید با Unsplash API

کمی به پایین اسکرول کنید و Access Key را که روی صفحه نمایش داده می شود کپی کنید:

14_دسترسی_کلید_کپی
دریافت کلید دسترسی از Unsplash API

در مرحله بعد، یک فایل .env جدید در پروژه خود ایجاد کنید و یک متغیر محیطی جدید با نام VITE_API_KEY اضافه کنید. همچنین مقدار کپی شده کلید API را به آن اختصاص دهید:

 VITE_API_KEY=A4UiJ5OIwL_4ccbCAE1ZXw3EgoNRotMbdNe12qtKHzM

مطمئن شوید که نام متغیر را با VITE_ شروع کرده اید تا در برنامه قابل دسترسی باشد.

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

15_api_key
ساختار فایل با فایل env

همچنین، حتماً .env در فایل .gitignore اضافه کنید تا زمانی که تغییرات به GitHub منتقل می‌شوند، فایل به GitHub فرستاده نشود.

اکنون به Unsplash Documentation رفته و روی قسمت Search photos by keyword کلیک کنید. و URL پایه API زیر را کپی کنید: https://api.unsplash.com/search/photos .

16_base_api_url
صفحه اسناد API تصاویر را جستجو کنید

اکنون، فایل 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 برقرار می شود و ما داده های گزینه انتخاب شده را دریافت می کنیم.

17_api_result
برقراری تماس API با کلیک روی گزینه جستجوی سریع

برای برقراری تماس API هنگامی که متن جستجو را وارد می کنیم و کلید enter را فشار می دهیم، باید تابع fetchImages را از تابع handleSearch نیز فراخوانی کنیم.

برای انجام این کار، مطابق شکل زیر یک فراخوانی به تابع fetchImages در داخل تابع handleSearch اضافه کنید:

 const handleSearch = (event) => { event.preventDefault(); console.log(searchInput.current.value); fetchImages(); };

اکنون، هنگامی که متن جستجو را وارد می کنیم و کلید Enter را فشار می دهیم، می توانید تماس API انجام شده در تب شبکه را مشاهده کنید.

18_نتایج_جستجو
برقراری تماس API هنگام وارد کردن متن در جعبه جستجو

نحوه ذخیره داده های API با استفاده از حالت

حالا بیایید تصاویری را که از API می آیند روی صفحه نمایش دهیم.

برای نمایش آنها بر روی صفحه، ابتدا باید داده های دریافتی از API را ذخیره کنیم.

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

19_پاسخ
پاسخ 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 مشاهده کنید که می‌توانیم از آنها برای نمایش تصاویر جداگانه استفاده کنیم.

20_api_response
ویژگی های پاسخ API

پس ، درست بعد از 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 برمی گردانیم.

حال اگر هر متنی را جستجو کنید، تصاویر را به درستی نمایش می دهید.

21_تصاویر_نمایش داده شده
تصاویر نمایش داده شده با کلیک بر روی نماد جستجوی سریع
22_تصاویر_نمایش داده شده
تصاویر نمایش داده شده با کلیک بر روی نماد جستجوی سریع
23_تصاویر_نمایش داده شده
تصاویر نمایش داده شده پس از وارد کردن عبارت جستجو

اکنون دکمه های قبلی و بعدی را اضافه می کنیم تا مجموعه های مختلف تصاویر را مشاهده کنیم.

پس ، ابتدا یک حالت جدید در کامپوننت 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);
24_صفحه بندی
نمایش مقدار فعلی صفحه در کنسول

همانطور که در بالا می بینید، در ابتدا برای صفحه اول، دکمه 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 را برای بارگذاری مجموعه بعدی تصاویر فراخوانی می کنیم.

اکنون، اگر برنامه را تحلیل کنید، تصاویر به درستی بارگذاری شده اند.

25_loading_next_images
بارگیری مجموعه بعدی تصاویر با استفاده از صفحه بندی

همانطور که در بالا می بینید، وقتی روی دکمه Previous یا Next کلیک می کنیم، تصاویر را به درستی بارگذاری می کنیم.

اما یک مسئله کوچک وجود دارد.

اگر در صفحه اول یا آخر نباشیم، دکمه های Previous و Next را می بینیم و وقتی قصد داریم عبارت دیگری را جستجو کنیم یا روی گزینه های جستجوی سریع کلیک می کنیم، همچنان دکمه Previous را می بینیم.

در حالت ایده آل، زمانی که عبارت دیگری را جستجو می کنیم یا روی گزینه جستجوی سریع دیگری کلیک می کنیم، باید از صفحه اول شروع کنیم، پس فقط دکمه Next باید قابل مشاهده باشد. اما در حال حاضر هر دو دکمه Previous و Next همانطور که در زیر می بینید قابل مشاهده هستند:

26_قبلی_نه_بازنشانی
مشکل با دکمه های قبلی که در جستجو پنهان نمی شوند

برای رفع این مشکل، زمانی که عبارت دیگری را جستجو می کنیم یا روی گزینه جستجوی سریع دیگری کلیک می کنیم، باید مقدار حالت 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(); };

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

27_صحیح_بارگذاری_فرست_صفحه
نسخه ی نمایشی دکمه قبلی پنهان در هر جستجو

کل فایل 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 را مطابق شکل زیر نصب کنید:

28_eslint_extension
برنامه گفت نی VS Code ESLint

پس از نصب افزونه، اگر فایل App.jsx را تحلیل کنید، بلافاصله یک خط زرد رنگ برای وابستگی page قلاب useEffect مشاهده خواهید کرد. اگر ماوس را روی آن قرار دهید، هشداری را مانند شکل زیر مشاهده خواهید کرد:

29_وابستگی_گمشده
ESLint Warning For useEffect Hook

همانطور که اخطار نشان می دهد، باید یک وابستگی fetchImages را در آرایه وابستگی اضافه کنیم.

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

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

برای رفع این مشکل می‌توانید روی لینک رفع سریع کلیک کنید و گزینه «به‌روزرسانی وابستگی‌ها» را مطابق شکل زیر انتخاب کنید:

30_updating_dependency
در حال به روز رسانی وابستگی useEffect Hook

تمام وابستگی های از دست رفته به طور خودکار در آرایه وابستگی اضافه می شوند.

همچنین در صورت تمایل می توانید وابستگی را به صورت دستی اضافه کنید.

با این حال، با این تغییر، یک هشدار زرد جدید برای تابع fetchImages مانند شکل زیر مشاهده خواهید کرد:

31_fetchimages_warning
هشدار ESLint برای استفاده برگشت

همانطور که قبلاً گفتم، در هر رندر مجدد کامپوننت، تابع 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 نخواهید دید.

32_بدون_هشدار
هشدار ESLint مربوط به useCallback رفع شد

با این حال، اگر کنسول مرورگر را تحلیل کنید، با خطا مواجه خواهید شد و چیزی در رابط کاربری نمایش داده نمی شود زیرا برنامه از کار افتاده است.

33_access_before_initialization
خطای کنسول مربوط به بیان تابع

خطاهایی دریافت می‌کنیم زیرا تابع fetchImages را با استفاده از نحو عبارت تابع اعلام کرده‌ایم، و توابع اعلام‌شده با استفاده از نحو عبارت تابع را نمی‌توان قبل از تعریف آنها فراخوانی کرد.

اختصاص یک تابع به یک متغیر، آن را به یک عبارت تابع تبدیل می کند.

همانطور که در تصویر زیر می بینید، ما تابع fetchImages را در خط شماره ۱۶ فراخوانی می کنیم و تابع را در خط شماره ۱۹ اعلام می کنیم و توابع اعلام شده با استفاده از نحو عبارت تابع قبل از اعلان قابل دسترسی نیستند.

34_function_expression_error
علت خطای کنسول

برای رفع این مشکل، باید تابع را قبل از فراخوانی آن اعلام کنیم. پس ، تابع 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 برقرار می کنیم که خوب نیست.

35_extra_api_call
نسخه ی نمایشی تماس های 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]);
36_no_api_calls
رفع مشکل تماس های API بدون هیچ مقدار

همانطور که در بالا مشاهده می کنید، ابتدا در بارگذاری صفحه و بدون وارد کردن مقدار، اگر کلید 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 می آیند نمایش می دهیم.

اگر برنامه را تحلیل کنید، خواهید دید که نشانگر بارگذاری به درستی نمایش داده می شود.

37_بارگذاری_نشان
در حال بارگیری نمایش نمایشی

با تشکر برای خواندن

برای این آموزش تمام شد. امیدوارم چیزهای زیادی از آن یاد گرفته باشید.

شما می توانید کد منبع کامل این برنامه را در این مخزن بیابید.

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 .