در قسمت قبل با کلیت کار با useEffect آشنا شدیم اما نکات و جزئیاتی در مورد useEffect باقی مانده است که دوست دارم آن ها را بیشتر توضیح بدهم. اولین مسئله این است که دقیقا مثل useState می توانید از useEffect چندین بار استفاده کنید و محدودیتی در تعداد استفاده از آن ندارید. به طور مثال یک useEffect دیگر را به کدهای جلسه قبل اضافه می کنم:
useEffect(() => { fetch('https://react-hooks-update.firebaseio.com/ingredients.json') .then(response => response.json()) .then(responseData => { const loadedIngredients = []; for (const key in responseData) { loadedIngredients.push({ id: key, title: responseData[key].title, amount: responseData[key].amount }); } setUserIngredients(loadedIngredients); }); }, []); useEffect(() => { console.log('RENDERING INGREDIENTS'); });
این یک دستور ساده console.log است. حالا اگر به مرورگر بروید و صفحه مرورگر را refresh کنید متوجه موضوع جالبی می شوید. با هر بار refresh کردن صفحه، این دستور console.log دوباره اجرا می شود. چرا؟ به دلیل اینکه پارامتر دوم useEffect را تعیین نکرده ایم. من با پاس دادن محتویات state به این دستور console.log یک وابستگی خارجی ایجاد می کنم تا بتوانیم با پارامتر دوم useEffect نیز کار کنیم:
useEffect(() => { console.log('RENDERING INGREDIENTS', userIngredients); }, [userIngredients]);
در اینجا مشخص کرده ایم که userIngredients وابستگی خارجی این useEffect است بنابراین این کد فقط زمانی اجرا می شود که userIngredients تغییر کند. از آنجا که userIngredients را از Firebase می گیریم، با هر بار refresh کردن صفحه باعث تغییر userIngredients شده و کد بالا را اجرا خواهیم کرد اما اگر userIngredients را به صورت ثابت داشتیم دیگر مشکلی وجود نداشت.
مسئله بعدی ما قسمت جست و جو در برنامه است (قسمت filter by id در برنامه). ما می خواهیم زمانی که کاربر چیزی را در این قسمت تایپ می کند، در قسمت loaded ingredients جست و جو انجام دهیم تا از بین انواع محتویات، مورد جست و جو شده را پیدا کنیم. برای انجام این کار به فایل Search.js بروید.
اولین کار مدیریت ورودی های کاربر است بنابراین باید از useState استفاده کنیم:
import React, { useState } from 'react';
سپس مثل همیشه از array destructuring استفاده می کنیم تا state و تابع مسئول تغییر آن را جداگانه داشته باشیم:
const [enteredFilter, setEnteredFilter] = useState('');
حالا state اولیه ما یک رشته خالی است. من این مقدار را به input درون JSX متصل می کنم:
<input type="text" value={enteredFilter} />
سپس باید مقدار تایپ شده توسط کاربر را دریافت کنیم. برای این کار مثل جلسات قبل از onChange استفاده می کنیم تا مقدار را دریافت کرده و با استفاده از تابع setEnteredFilter آن را به state اضافه می کنیم:
<input type="text" value={enteredFilter} onChange={event => setEnteredFilter(event.target.value)} />
با این کار هر زمان که کاربر چیزی را در فیلد جست و جو وارد کند، ما آن را در state ذخیره می کنیم. حالا برای عملیات جست و جو می توانیم از useEffect استفاده کنیم بنابراین آن را وارد فایل می کنیم:
import React, { useState, useEffect } from 'react';
من می خواهم کاری کنم که با تایپ کاربر، ingredients خود را از firebase دریافت کرده و سپس کامپوننت ingredients.js را فعال کنیم. چرا؟ به دلیل اینکه کامپوننت search درون JSX آن قرار دارد:
<section> <Search />
بنابراین زمانی که کاربر مقداری را در فیلد جست و جو وارد کند، باید آن را در firebase جست و جو کنیم و سپس loaded ingredients در فایل ingredients.js را به روز رسانی کنیم تا فقط مورد جست و جو شده را نمایش بدهد. ممکن است در ابتدا کمی گنگ باشد اما در ادامه کاملا متوجه حرف من می شوید.
معمولا برای انجام چنین کاری از props استفاده می کنیم بنابراین من هم همین کار را انجام می دهم:
useEffect(() => { fetch('https://react-hooks-update.firebaseio.com/ingredients.json') .then(response => response.json()) .then(responseData => { const loadedIngredients = []; for (const key in responseData) { loadedIngredients.push({ id: key, title: responseData[key].title, amount: responseData[key].amount }); } props.onLoadIngredients(loadedIngredients); }); }, [enteredFilter]);
من یک useEffect را درون Search.js تعریف کرده ام که دقیقا همان منطق دریافت جلسه قبل (کامپوننت Ingredients.js را دارد اما به جای آنکه state را تغییر بدهم از onLoadIngredients استفاده کرده ام که قرار است با prop ها به ما پاس داده شود. همچنین enteredFilter را نیز به عنوان وابستگی خارجی به پارامتر دوم پاس داده ام. قدم بعدی رفتن به فایل Ingredients.js و پاس دادن این prop می باشد:
<section> <Search onLoadIngredients={filteredIngredientsHandler} />
filteredIngredientsHandler قرار است تابعی باشد که کار ما را انجام دهد. من هنوز چنین تابعی تعریف نکرده ام بنابراین آن را درون همان فایل Ingredients.js تعریف می کنم:
const filteredIngredientsHandler = filteredIngredients => { setUserIngredients(filteredIngredients); }
کار این تابع بسیار ساده است. محتویات ما درون state را با filteredIngredients جایگزین می کند. در حال حاضر اگر به فایل Search.js برگردید یک هشدار دریافت می کنید. چرا که از props استفاده کرده ایم اما آن را به عنوان وابستگی خارجی ذکر نکرده ایم بنابراین این کار را نیز انجام می دهیم:
useEffect(() => { fetch('https://react-hooks-update.firebaseio.com/ingredients.json') .then(response => response.json()) .then(responseData => { const loadedIngredients = []; for (const key in responseData) { loadedIngredients.push({ id: key, title: responseData[key].title, amount: responseData[key].amount }); } props.onLoadIngredients(loadedIngredients); }); }, [enteredFilter, props]);
اگر خود props را به شکل بالا به پارامتر دوم بدهیم با مشکل مواجه می شویم. اگر props را به آن بدهیم با هر بار تغییر props های ارسالی به این کامپوننت (از هر نوعی باشد) یا تغییر props های پدر، باعث اجرای دوباره useEffect می شویم در حالی که ما فقط می خواهیم زمانی useEffect دوباره اجرا شود که onLoadIngredients تغییر کند.
برای حل مشکل می توانیم از مفهومی به نام object destructuring (که دقیقا شبیه به array destructuring می باشد) استفاده کنیم:
const Search = React.memo(props => { const { onLoadIngredients } = props; const [enteredFilter, setEnteredFilter] = useState(''); useEffect(() => { // بقیه کدها //
در این کد (خط دوم از کد بالا) گفته ام از درون props به دنبال شیء ای به نام onLoadIngredients بگرد. onLoadIngredients به صورت یک key در شیء props ذخیره می شود اما یادتان باشد که توابع در جاوا اسکریپت شیء هستند، بنابراین می توانند تغییر کنند و به شکل بالا آن ها را پیدا کرد.
حالا (دقیقا مانند array destructuring) نیازی به صدا زدن props نیست بلکه می توان onLoadIngredients را مستقیما صدا زد:
useEffect(() => { fetch('https://react-hooks-update.firebaseio.com/ingredients.json') .then(response => response.json()) .then(responseData => { const loadedIngredients = []; for (const key in responseData) { loadedIngredients.push({ id: key, title: responseData[key].title, amount: responseData[key].amount }); } onLoadIngredients(loadedIngredients); }); }, [enteredFilter, onLoadIngredients]);
به طور خلاصه تا این قسمت به صورت زیر عمل کرده ایم:
بنابراین تنها کار باقی مانده ویرایش درخواست ارسالی به firebase برای جست و جوی عبارت تایپ شده توسط کاربر است. برای این کار باید query param هایی را به درخواست خودمان متصل کنیم:
const Search = React.memo(props => { const { onLoadIngredients } = props; const [enteredFilter, setEnteredFilter] = useState(''); useEffect(() => { const query = enteredFilter.length === 0 ? '' :
من در اینجا ثابتی به نام query را تعریف کرده ام که می گوید اگر طول enteredFilter صفر باشد (یعنی کاربر چیزی را در فیلد جست و جو تایپ نکرده باشد) مقدار ثابت query را برابر یک رشته خالی قرار بده و در غیر این صورت (علامت دو نقطه) پارامتر کوئری را بدین شکل تعریف می کنیم:
const Search = React.memo(props => { const { onLoadIngredients } = props; const [enteredFilter, setEnteredFilter] = useState(''); useEffect(() => { const query = enteredFilter.length === 0 ? '' : `?orderBy="title"&equalTo="${enteredFilter}"`;
این ساختار مخصوص firebase است که باید با علامت سوال شروع شود و می گوییم orderBy (یعنی «مرتب کن بر اساس...») سپس با double quotes مقدار title را پاس می دهیم. یعنی داده هایمان را بر اساس title ها مرتب کن یا به زبان ساده تر مقداری که می گویم را در title جست و جو کن. سپس یک علامت & را گذاشته و عبارت equalTo (یعنی «برابر با») را می نویسیم. کل این کوئری به زبان ساده می گوید که از بین ingredient های من به دنبال آن هایی بگرد که در در title خود مقدار جست و جو شده توسط کاربر (enteredFilter) را داشته باشند.
حالا باید این query را به انتهای آدرس خودمان متصل کنیم:
const Search = React.memo(props => { const { onLoadIngredients } = props; const [enteredFilter, setEnteredFilter] = useState(''); useEffect(() => { const query = enteredFilter.length === 0 ? '' : `?orderBy="title"&equalTo="${enteredFilter}"`; fetch('https://react-hooks-update.firebaseio.com/ingredients.json' + query) .then(response => response.json()) // بقیه کدها //
همانطور که می بینید با علامت + و به سادگی query را به انتهای درخواست اضافه کرده ام. البته برای فعال کردن قابلیت جست و جو در پایگاه داده باید به صفحه پایگاه داده خود در firebase رفته و اجازه عملیات filtering را بدهید. برای این کار مثل من کد زیر را وارد قسمت rules از پایگاه داده خود کنید:
دستورات read و write از قبل در این قسمت موجود هستند، بنابراین شما باید قسمت ingredients را اضافه کنید. این کد به Firebase می گوید اجازه جست و جو در title ها را به ما بده. این قسمت (ingredients)، از انتهای آدرس ما گرفته شده است:
https://react-hooks-update.firebaseio.com/ingredients.json
قبلا توضیح داده بودم که این URL در هنگام ساختن یک جدول در پایگاه داده Firebase به شما داده می شود. اگر کدهای بالا را ذخیره کنید باز هم به مشکل حلقه بی نهایت برمی خوریم. به نظر شما مشکل کجاست؟ در قسمت بعد این مورد را بررسی خواهیم کرد.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.