بیاموزید چگونه با ترکیب هوکهای سفارشی ریاکت، منطقهای پیچیده را انتزاعی کرده، قابلیت استفاده مجدد کد و نگهداریپذیری پروژهها را بهبود بخشید. شامل مثالهای کاربردی و بهترین شیوهها.
ترکیب هوکهای سفارشی ریاکت: تسلط بر انتزاع منطقهای پیچیده
هوکهای سفارشی ریاکت ابزاری قدرتمند برای کپسولهسازی و استفاده مجدد از منطقهای حالتمند (stateful) در برنامههای ریاکت شما هستند. با این حال، با افزایش پیچیدگی برنامههای شما، منطق درون هوکهای سفارشی نیز پیچیدهتر میشود. این امر میتواند منجر به هوکهای یکپارچه (monolithic) شود که درک، تست و نگهداری آنها دشوار است. ترکیب هوکهای سفارشی با امکان شکستن منطقهای پیچیده به هوکهای کوچکتر، قابل مدیریتتر و قابل استفاده مجدد، راه حلی برای این مشکل ارائه میدهد.
ترکیب هوکهای سفارشی چیست؟
ترکیب هوکهای سفارشی، عمل ترکیب چندین هوک سفارشی کوچکتر برای ایجاد قابلیتهای پیچیدهتر است. به جای ایجاد یک هوک بزرگ و واحد که همه چیز را مدیریت کند، شما چندین هوک کوچکتر ایجاد میکنید که هر کدام مسئول یک جنبه خاص از منطق هستند. سپس این هوکهای کوچکتر میتوانند برای دستیابی به عملکرد مورد نظر با یکدیگر ترکیب شوند.
این فرآیند را مانند ساخت و ساز با آجرهای لگو در نظر بگیرید. هر آجر (هوک کوچک) عملکرد مشخصی دارد و شما آنها را به روشهای مختلف ترکیب میکنید تا سازههای پیچیده (ویژگیهای بزرگتر) بسازید.
مزایای ترکیب هوکهای سفارشی
- بهبود قابلیت استفاده مجدد کد: هوکهای کوچکتر و متمرکزتر ذاتاً در کامپوننتهای مختلف و حتی پروژههای متفاوت، قابلیت استفاده مجدد بیشتری دارند.
- افزایش نگهداریپذیری: شکستن منطق پیچیده به واحدهای کوچکتر و مستقل، درک، اشکالزدایی و اصلاح کد شما را آسانتر میکند. تغییرات در یک هوک کمتر احتمال دارد بر سایر بخشهای برنامه شما تأثیر بگذارد.
- افزایش تستپذیری: هوکهای کوچکتر به راحتی به صورت مجزا قابل تست هستند که منجر به کدی قویتر و قابل اعتمادتر میشود.
- سازماندهی بهتر کد: ترکیب، یک کدبیس ماژولار و سازمانیافتهتر را تشویق میکند و درک روابط بین بخشهای مختلف برنامه شما را آسانتر میسازد.
- کاهش تکرار کد: با استخراج منطق مشترک به هوکهای قابل استفاده مجدد، تکرار کد را به حداقل میرسانید که منجر به یک کدبیس مختصرتر و قابل نگهداریتر میشود.
چه زمانی از ترکیب هوکهای سفارشی استفاده کنیم؟
شما باید استفاده از ترکیب هوکهای سفارشی را در موارد زیر در نظر بگیرید:
- یک هوک سفارشی واحد بیش از حد بزرگ و پیچیده شده است.
- متوجه میشوید که منطق مشابهی را در چندین هوک سفارشی یا کامپوننت تکرار میکنید.
- میخواهید تستپذیری هوکهای سفارشی خود را بهبود ببخشید.
- میخواهید یک کدبیس ماژولارتر و با قابلیت استفاده مجدد بالاتر ایجاد کنید.
اصول اولیه ترکیب هوکهای سفارشی
در اینجا چند اصل کلیدی برای راهنمایی رویکرد شما به ترکیب هوکهای سفارشی آورده شده است:
- اصل مسئولیت واحد (Single Responsibility Principle): هر هوک سفارشی باید یک مسئولیت واحد و به خوبی تعریف شده داشته باشد. این امر درک، تست و استفاده مجدد آنها را آسانتر میکند.
- جداسازی دغدغهها (Separation of Concerns): جنبههای مختلف منطق خود را به هوکهای مختلف جدا کنید. به عنوان مثال، ممکن است یک هوک برای واکشی دادهها، دیگری برای مدیریت وضعیت و دیگری برای مدیریت اثرات جانبی (side effects) داشته باشید.
- قابلیت ترکیب (Composability): هوکهای خود را طوری طراحی کنید که به راحتی با هوکهای دیگر قابل ترکیب باشند. این امر اغلب شامل بازگرداندن دادهها یا توابعی است که میتوانند توسط هوکهای دیگر استفاده شوند.
- قراردادهای نامگذاری (Naming Conventions): از نامهای واضح و توصیفی برای هوکهای خود استفاده کنید تا هدف و عملکرد آنها را مشخص کند. یک قرارداد رایج، پیشوند `use` برای نام هوکها است.
الگوهای رایج ترکیب
الگوهای متعددی برای ترکیب هوکهای سفارشی وجود دارد. در اینجا برخی از رایجترین آنها آورده شده است:
۱. ترکیب ساده هوک
این ابتداییترین شکل ترکیب است، جایی که یک هوک به سادگی هوک دیگری را فراخوانی کرده و از مقدار بازگشتی آن استفاده میکند.
مثال: تصور کنید یک هوک برای واکشی دادههای کاربر و دیگری برای فرمتبندی تاریخ دارید. میتوانید این هوکها را ترکیب کرده و یک هوک جدید ایجاد کنید که دادههای کاربر را واکشی کرده و تاریخ ثبتنام کاربر را فرمتبندی میکند.
import { useState, useEffect } from 'react';
function useUserData(userId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const jsonData = await response.json();
setData(jsonData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
fetchData();
}, [userId]);
return { data, loading, error };
}
function useFormattedDate(dateString) {
try {
const date = new Date(dateString);
const formattedDate = date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
return formattedDate;
} catch (error) {
console.error("Error formatting date:", error);
return "Invalid Date";
}
}
function useUserWithFormattedDate(userId) {
const { data, loading, error } = useUserData(userId);
const formattedRegistrationDate = data ? useFormattedDate(data.registrationDate) : null;
return { ...data, formattedRegistrationDate, loading, error };
}
export default useUserWithFormattedDate;
توضیح:
useUserDataدادههای کاربر را از یک API واکشی میکند.useFormattedDateیک رشته تاریخ را به فرمتی کاربرپسند تبدیل میکند. این هوک خطاهای احتمالی در تجزیه تاریخ را به خوبی مدیریت میکند. آرگومانundefinedبهtoLocaleDateStringاز موقعیت مکانی (locale) کاربر برای فرمتبندی استفاده میکند.useUserWithFormattedDateهر دو هوک را با هم ترکیب میکند. ابتدا ازuseUserDataبرای واکشی دادههای کاربر استفاده میکند. سپس، اگر دادهها در دسترس باشند، ازuseFormattedDateبرای فرمتبندیregistrationDateاستفاده میکند. در نهایت، دادههای اصلی کاربر را به همراه تاریخ فرمتبندی شده، وضعیت بارگذاری و هرگونه خطای احتمالی برمیگرداند.
۲. ترکیب هوک با وضعیت مشترک
در این الگو، چندین هوک یک وضعیت مشترک را به اشتراک گذاشته و تغییر میدهند. این کار را میتوان با استفاده از useContext یا با پاس دادن وضعیت و توابع تنظیمکننده (setter) بین هوکها انجام داد.
مثال: ساخت یک فرم چندمرحلهای را تصور کنید. هر مرحله میتواند هوک مخصوص به خود را برای مدیریت فیلدهای ورودی و منطق اعتبارسنجی آن مرحله داشته باشد، اما همه آنها یک وضعیت فرم مشترک را به اشتراک میگذارند که توسط یک هوک والد با استفاده از useReducer و useContext مدیریت میشود.
import React, { createContext, useContext, useReducer } from 'react';
// Define the initial state
const initialState = {
step: 1,
name: '',
email: '',
address: ''
};
// Define the actions
const ACTIONS = {
NEXT_STEP: 'NEXT_STEP',
PREVIOUS_STEP: 'PREVIOUS_STEP',
UPDATE_FIELD: 'UPDATE_FIELD'
};
// Create the reducer
function formReducer(state, action) {
switch (action.type) {
case ACTIONS.NEXT_STEP:
return { ...state, step: state.step + 1 };
case ACTIONS.PREVIOUS_STEP:
return { ...state, step: state.step - 1 };
case ACTIONS.UPDATE_FIELD:
return { ...state, [action.payload.field]: action.payload.value };
default:
return state;
}
}
// Create the context
const FormContext = createContext();
// Create a provider component
function FormProvider({ children }) {
const [state, dispatch] = useReducer(formReducer, initialState);
const value = {
state,
dispatch,
nextStep: () => dispatch({ type: ACTIONS.NEXT_STEP }),
previousStep: () => dispatch({ type: ACTIONS.PREVIOUS_STEP }),
updateField: (field, value) => dispatch({ type: ACTIONS.UPDATE_FIELD, payload: { field, value } })
};
return (
{children}
);
}
// Custom hook for accessing the form context
function useFormContext() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within a FormProvider');
}
return context;
}
// Custom hook for Step 1
function useStep1() {
const { state, updateField } = useFormContext();
const updateName = (value) => updateField('name', value);
return {
name: state.name,
updateName
};
}
// Custom hook for Step 2
function useStep2() {
const { state, updateField } = useFormContext();
const updateEmail = (value) => updateField('email', value);
return {
email: state.email,
updateEmail
};
}
// Custom hook for Step 3
function useStep3() {
const { state, updateField } = useFormContext();
const updateAddress = (value) => updateField('address', value);
return {
address: state.address,
updateAddress
};
}
export { FormProvider, useFormContext, useStep1, useStep2, useStep3 };
توضیح:
- یک
FormContextبا استفاده ازcreateContextبرای نگهداری وضعیت فرم و تابع dispatch ایجاد میشود. - یک
formReducerبهروزرسانیهای وضعیت فرم را با استفاده ازuseReducerمدیریت میکند. اکشنهایی مانندNEXT_STEP،PREVIOUS_STEPوUPDATE_FIELDبرای تغییر وضعیت تعریف شدهاند. - کامپوننت
FormProviderکانتکست فرم را برای فرزندان خود فراهم میکند و وضعیت و dispatch را در دسترس تمام مراحل فرم قرار میدهد. همچنین توابع کمکی برای `nextStep`، `previousStep` و `updateField` را برای سادهسازی ارسال اکشنها در اختیار میگذارد. - هوک
useFormContextبه کامپوننتها اجازه میدهد به مقادیر کانتکست فرم دسترسی پیدا کنند. - هر مرحله (
useStep1،useStep2،useStep3) هوک مخصوص به خود را برای مدیریت ورودیهای مربوط به آن مرحله ایجاد میکند و ازuseFormContextبرای دریافت وضعیت و تابع dispatch برای بهروزرسانی آن استفاده میکند. هر مرحله فقط دادهها و توابع مربوط به خود را در معرض نمایش قرار میدهد و از اصل مسئولیت واحد پیروی میکند.
۳. ترکیب هوک با مدیریت چرخه حیات
این الگو شامل هوکهایی است که فازهای مختلف چرخه حیات یک کامپوننت، مانند mount، update و unmount را مدیریت میکنند. این کار اغلب با استفاده از useEffect در هوکهای ترکیب شده انجام میشود.
مثال: کامپوننتی را در نظر بگیرید که باید وضعیت آنلاین/آفلاین بودن را ردیابی کند و همچنین هنگام unmount شدن، عملیات پاکسازی انجام دهد. میتوانید هوکهای جداگانهای برای هر یک از این وظایف ایجاد کرده و سپس آنها را ترکیب کنید.
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
return () => {
document.title = 'Original Title'; // Revert to a default title on unmount
};
}, [title]);
}
function useAppLifecycle(title) {
const isOnline = useOnlineStatus();
useDocumentTitle(title);
return isOnline; // Return the online status
}
export { useAppLifecycle, useOnlineStatus, useDocumentTitle };
توضیح:
useOnlineStatusوضعیت آنلاین بودن کاربر را با استفاده از رویدادهایonlineوofflineردیابی میکند. هوکuseEffectشنوندگان رویداد را هنگام mount شدن کامپوننت تنظیم کرده و هنگام unmount شدن آنها را پاکسازی میکند.useDocumentTitleعنوان سند را بهروز میکند. همچنین هنگام unmount شدن کامپوننت، عنوان را به یک مقدار پیشفرض بازمیگرداند تا از باقی ماندن مشکلات مربوط به عنوان جلوگیری شود.useAppLifecycleهر دو هوک را ترکیب میکند. ازuseOnlineStatusبرای تعیین آنلاین بودن کاربر و ازuseDocumentTitleبرای تنظیم عنوان سند استفاده میکند. هوک ترکیبی، وضعیت آنلاین بودن را برمیگرداند.
مثالهای عملی و موارد استفاده
۱. بینالمللیسازی (i18n)
مدیریت ترجمهها و تغییر زبان میتواند پیچیده شود. میتوانید از ترکیب هوک برای جداسازی دغدغهها استفاده کنید:
useLocale(): زبان (locale) فعلی را مدیریت میکند.useTranslations(): ترجمههای مربوط به زبان فعلی را واکشی و فراهم میکند.useTranslate(key): هوکی که یک کلید ترجمه میگیرد و رشته ترجمه شده را برمیگرداند، و از هوکuseTranslationsبرای دسترسی به ترجمهها استفاده میکند.
این به شما امکان میدهد به راحتی زبانها را تغییر دهید و به ترجمهها در سراسر برنامه خود دسترسی داشته باشید. استفاده از کتابخانههایی مانند i18next به همراه هوکهای سفارشی برای مدیریت منطق ترجمه را در نظر بگیرید. به عنوان مثال، useTranslations میتواند ترجمهها را بر اساس زبان انتخاب شده از فایلهای JSON به زبانهای مختلف بارگذاری کند.
۲. اعتبارسنجی فرم
فرمهای پیچیده اغلب به اعتبارسنجی گسترده نیاز دارند. میتوانید از ترکیب هوک برای ایجاد منطق اعتبارسنجی قابل استفاده مجدد استفاده کنید:
useInput(initialValue): وضعیت یک فیلد ورودی واحد را مدیریت میکند.useValidator(value, rules): یک فیلد ورودی واحد را بر اساس مجموعهای از قوانین (مانند: required, email, minLength) اعتبارسنجی میکند.useForm(fields): وضعیت و اعتبارسنجی کل فرم را مدیریت میکند وuseInputوuseValidatorرا برای هر فیلد ترکیب میکند.
این رویکرد قابلیت استفاده مجدد کد را ترویج میدهد و افزودن یا تغییر قوانین اعتبارسنجی را آسانتر میکند. کتابخانههایی مانند Formik یا React Hook Form راهحلهای از پیش ساختهای را ارائه میدهند اما میتوان آنها را با هوکهای سفارشی برای نیازهای اعتبارسنجی خاص تکمیل کرد.
۳. واکشی و کش کردن دادهها
مدیریت واکشی دادهها، کش کردن و مدیریت خطا را میتوان با ترکیب هوک سادهتر کرد:
useFetch(url): دادهها را از یک URL مشخص واکشی میکند.useCache(key, fetchFunction): نتیجه یک تابع واکشی را با استفاده از یک کلید کش میکند.useData(url, options):useFetchوuseCacheرا برای واکشی دادهها و کش کردن نتایج ترکیب میکند.
این به شما امکان میدهد به راحتی دادههایی که به طور مکرر به آنها دسترسی پیدا میکنید را کش کرده و عملکرد را بهبود ببخشید. کتابخانههایی مانند SWR (Stale-While-Revalidate) و React Query راهحلهای قدرتمند واکشی و کش کردن دادهها را ارائه میدهند که میتوان آنها را با هوکهای سفارشی گسترش داد.
۴. احراز هویت
مدیریت منطق احراز هویت میتواند پیچیده باشد، به خصوص هنگام کار با روشهای مختلف احراز هویت (مانند JWT، OAuth). ترکیب هوک میتواند به جداسازی جنبههای مختلف فرآیند احراز هویت کمک کند:
useAuthToken(): توکن احراز هویت را مدیریت میکند (مثلاً ذخیره و بازیابی آن از حافظه محلی).useUser(): اطلاعات کاربر فعلی را بر اساس توکن احراز هویت واکشی و فراهم میکند.useAuth(): توابع مربوط به احراز هویت مانند ورود، خروج و ثبتنام را با ترکیب هوکهای دیگر فراهم میکند.
این رویکرد به شما امکان میدهد به راحتی بین روشهای مختلف احراز هویت جابجا شوید یا ویژگیهای جدیدی به فرآیند احراز هویت اضافه کنید. کتابخانههایی مانند Auth0 و Firebase Authentication میتوانند به عنوان بکاند برای مدیریت حسابهای کاربری و احراز هویت استفاده شوند و هوکهای سفارشی میتوانند برای تعامل با این سرویسها ایجاد شوند.
بهترین شیوهها برای ترکیب هوکهای سفارشی
- هوکها را متمرکز نگه دارید: هر هوک باید یک هدف واضح و مشخص داشته باشد.
- از تودرتوی عمیق خودداری کنید: تعداد سطوح ترکیب را محدود کنید تا از دشوار شدن درک کد خود جلوگیری کنید. اگر یک هوک بیش از حد پیچیده شد، آن را بیشتر تجزیه کنید.
- هوکهای خود را مستند کنید: مستندات واضح و مختصری برای هر هوک ارائه دهید و هدف، ورودیها و خروجیهای آن را توضیح دهید. این امر به ویژه برای هوکهایی که توسط توسعهدهندگان دیگر استفاده میشوند، مهم است.
- هوکهای خود را تست کنید: برای هر هوک تستهای واحد بنویسید تا از عملکرد صحیح آن اطمینان حاصل کنید. این امر به ویژه برای هوکهایی که وضعیت را مدیریت میکنند یا اثرات جانبی انجام میدهند، مهم است.
- استفاده از یک کتابخانه مدیریت وضعیت را در نظر بگیرید: برای سناریوهای پیچیده مدیریت وضعیت، استفاده از کتابخانهای مانند Redux، Zustand یا Jotai را در نظر بگیرید. این کتابخانهها ویژگیهای پیشرفتهتری برای مدیریت وضعیت ارائه میدهند و میتوانند ترکیب هوکها را سادهتر کنند.
- به مدیریت خطا فکر کنید: مدیریت خطای قوی را در هوکهای خود پیادهسازی کنید تا از رفتار غیرمنتظره جلوگیری کنید. برای گرفتن خطاها و ارائه پیامهای خطای آموزنده، از بلوکهای try-catch استفاده کنید.
- عملکرد را در نظر بگیرید: به پیامدهای عملکردی هوکهای خود توجه داشته باشید. از رندرهای مجدد غیرضروری خودداری کنید و کد خود را برای عملکرد بهینه کنید. در صورت لزوم از React.memo، useMemo و useCallback برای بهینهسازی عملکرد استفاده کنید.
نتیجهگیری
ترکیب هوکهای سفارشی ریاکت یک تکنیک قدرتمند برای انتزاع منطق پیچیده و بهبود قابلیت استفاده مجدد کد، نگهداریپذیری و تستپذیری است. با شکستن وظایف پیچیده به هوکهای کوچکتر و قابل مدیریتتر، میتوانید یک کدبیس ماژولارتر و سازمانیافتهتر ایجاد کنید. با پیروی از بهترین شیوههای ذکر شده در این مقاله، میتوانید به طور مؤثر از ترکیب هوکهای سفارشی برای ساخت برنامههای ریاکت قوی و مقیاسپذیر استفاده کنید. به یاد داشته باشید که همیشه وضوح و سادگی را در کد خود در اولویت قرار دهید و از آزمایش الگوهای مختلف ترکیب برای یافتن بهترین راهحل برای نیازهای خاص خود نترسید.