کامپوننتهای Higher Order (یا به اختصار HOC) از الگوهای قدرتمند در React محسوب میشوند که به توسعهدهندگان اجازه میدهند بدون نیاز به ویرایش مستقیم کامپوننت اصلی، قابلیتهای جدیدی به آن اضافه کنند.
این الگوها راهکاری قابلاستفاده مجدد برای مدیریت دغدغههای مشترکی مانند احراز هویت، ثبت لاگ یا مدیریت state سراسری فراهم میکنند.
با وجود اینکه هوکها تا حد زیادی جایگزین HOCها برای بازاستفاده از منطق شدهاند، کامپوننتهای Higher Order همچنان در برخی شرایط مزایای منحصربهفردی دارند؛ به ویژه هنگام کار با کدهای قدیمی یا زمانی که نیاز به ایجاد تغییرات پیچیده در رفتار کامپوننتها وجود دارد.
کامپوننت Higher Order در React، تابعی است که یک کامپوننت را به عنوان ورودی دریافت میکند و یک کامپوننت جدید و توسعهیافته را به عنوان خروجی بازمیگرداند.
هر دو الگوی HOC و هوک، منطق دارای state را در خود نگه میدارند، اما این کار را به روشهای متفاوتی انجام میدهند و برای موارد استفاده مختلفی مناسب هستند.
برای درک بهتر تفاوت میان این دو الگو، در ادامه یک ویژگی شمارنده ساده را با دو روش پیادهسازی کردهایم: یکی با استفاده از HOC و دیگری با استفاده از یک هوک سفارشی.
// HOC that adds counter functionality to a component const withCounter = (WrappedComponent) => { return function CounterWrapper(props) { const [count, setCount] = useState(0); return ( <WrappedComponent count={count} increment={() => setCount(prev => prev + 1)} {...props} /> ); }; };
// Custom Hook that provides counter functionality const useCounter = () => { const [count, setCount] = useState(0); return { count, increment: () => setCount(prev => prev + 1) }; }; // Usage const Counter = () => { const {count, increment} = useCounter(); return ( <> <button>Increment</button> <p>Clicked:{count}</p> </> ) }
در حالی که هر دو روش عملکردی مشابه دارند، تفاوت اصلی آنها در ساختار پیادهسازی است:
الگوی HOC یک کامپوننت موجود را درون یک کامپوننت دیگر قرار میدهد تا قابلیتهای بیشتری به آن بیفزاید، در حالی که هوک سفارشی منطق قابل استفاده مجدد را جدا میکند و به شکلی تمیز، بدون تغییر در ساختار سلسله مراتبی کامپوننت، مورد استفاده قرار میگیرد.
طبق مستندات رسمی React، یک کامپوننت Higher Order معمولاً اینگونه تعریف میشود:
«کامپوننت Higher Order در React تابعی است که یک کامپوننت را به عنوان ورودی دریافت کرده و یک کامپوننت جدید بازمیگرداند.»
اگر بخواهیم این تعریف را به صورت کدی بیان کنیم، به شکل زیر خواهد بود:
const newComponent = higherFunction(WrappedComponent);
در این خط کد:
newComponent
: کامپوننت جدید و توسعهیافته استhigherFunction
: تابعی است که WrappedComponent
را توسعه میدهدWrappedComponent
: کامپوننت پایهای است که قصد افزودن قابلیتهای جدید به آن داریمبرای ساخت یک HOC، ابتدا باید تابعی تعریف کنیم که کامپوننت پایه را به عنوان آرگومان دریافت کرده و یک کامپوننت جدید با قابلیتهای اضافهشده را بازگرداند.
در یک HOC فانکشنال میتوانیم از هوکها برای مدیریت state و side effectها استفاده کنیم. به عنوان مثال:
import React, { useState, useEffect } from 'react'; const withEnhancement = (BaseComponent) => { return function EnhancedComponent(props) { // HOC-specific logic using hooks return <BaseComponent {...props} />; }; };
درون تابع EnhancedComponent
میتوانیم از هوکهایی مانند useState
، useEffect
و useRef
برای مدیریت state و انجام عملیات جانبی استفاده کنیم تا رفتارهای جدیدی به کامپوننت اضافه شود:
const withEnhancement = (BaseComponent) => { return function EnhancedComponent(props) { const [count, setCount] = useState(0); useEffect(() => { // Perform side effects here }, [count]); return <BaseComponent count={count} setCount={setCount} {...props} />; }; };
برای استفاده از HOC ساخته شده، کافی است کامپوننت پایه را به عنوان ورودی به تابع HOC ارسال کنیم. نتیجه، یک کامپوننت جدید با قابلیتهای توسعهیافته خواهد بود:
const EnhancedComponent = withEnhancement(BaseComponent);
کامپوننتی که با استفاده از HOC ساخته شده، مانند سایر کامپوننتهای React قابل استفاده است، با این تفاوت که قابلیتهای جدیدی از طریق HOC به آن اضافه شدهاند:
function App() { return <EnhancedComponent />; }
در بخش بعدی مقاله، به بررسی یک نمونه کاربردی از HOCها در عمل خواهیم پرداخت.
در این بخش به یک مثال عملی از استفاده از HOCها در پروژههای React میپردازیم.
ابتدا باید یک پروژه خالی React ایجاد کنیم. برای این کار، دستورات زیر را اجرا میکنیم:
npx create-react-app hoc-tutorial cd hoc-tutorial #navigate to the project folder. cd src #go to codebase mkdir components #will hold all our custom components
در این مقاله، دو کامپوننت سفارشی ایجاد میکنیم تا کاربرد HOC را به صورت عملی نمایش دهیم:
ClickIncrease.js
: این کامپوننت شامل یک دکمه و یک متن است. با کلیک روی دکمه (event onClick
)، مقدار ویژگی fontSize
متن افزایش مییابد.HoverIncrease.js
: مشابه ClickIncrease
است، با این تفاوت که با رویداد onMouseOver
واکنش نشان میدهد.وارد پوشه components
میشویم و این دو فایل را ایجاد میکنیم. در پایان، ساختار فایلهای پروژه باید به شکل زیر باشد:
📁 public 📁 src 📁 components 📄 ClickIncrease.js 📄 HoverIncrease.js 📄 App.js 📄 index.js 📄 styles.css 📄 package.json
در فایل ClickIncrease.js
کد زیر را مینویسیم:
// File: components/ClickIncrease.js import React, { useState } from 'react'; function ClickIncrease() { const [fontSize, setFontSize] = useState(10); // Set initial value to 10. return ( <button onClick={() => setFontSize(size => size + 1)}> Increase with click </button> <p style={{ fontSize: `${fontSize}px` }}> Size of font: {fontSize}px </p> ); } export default ClickIncrease;
سپس در فایل HoverIncrease.js
کد زیر را قرار میدهیم:
// File: components/HoverIncrease.js import React, { useState } from 'react'; function HoverIncrease() { const [fontSize, setFontSize] = useState(10); return ( <div onMouseOver={() => setFontSize(size => size + 1)}> <p style={{ fontSize: `${fontSize}px` }}> Size of font: {fontSize}px </p> </div> ); } export default HoverIncrease;
در نهایت، برای نمایش این دو کامپوننت در رابط کاربری، آنها را در فایل App.js
رندر میکنیم:
// File: App.js import React from 'react'; import ClickIncrease from './components/ClickIncrease'; import HoverIncrease from './components/HoverIncrease'; function App() { return ( <div> <ClickIncrease /> <HoverIncrease /> </div> ); } export default App;
در پوشه components
، فایلی با نام withCounter.js
ایجاد کرده، سپس کد ابتدایی زیر را در آن قرار میدهیم:
import React from "react"; const UpdatedComponent = (OriginalComponent) => { function NewComponent(props) { //render OriginalComponent and pass on its props. return ; } return NewComponent; }; export default UpdatedComponent;
در ابتدا، تابعی به نام UpdatedComponent
تعریف شده است که به عنوان ورودی، یک آرگومان به نام OriginalComponent
دریافت میکند. این آرگومان همان کامپوننت React است که قصد داریم آن را با قابلیتهای جدید wrap کنیم.
در مرحله بعد، به React دستور دادهایم که OriginalComponent
را در رابط کاربری رندر کند. پیادهسازی قابلیتهای افزوده را در ادامه مقاله انجام خواهیم داد.
برای استفاده از HOC ایجاد شده، ابتدا وارد فایل HoverIncrease.js
میشویم و کد زیر را به آن اضافه میکنیم:
import withCounter from "./withCounter.js" //import the withCounter function //..further code .. function HoverIncrease() { //..further code } //replace your 'export' statement with: export default withCounter(HoverIncrease); //We have now converted HoverIncrease to an HOC function.
در این قطعه کد، HoverIncrease
را با تابع withCounter
wrap کردهایم تا به یک کامپوننت Higher Order تبدیل شود.
سپس، دقیقاً همان مراحل را برای ماژول ClickIncrease
نیز انجام میدهیم:
//file name: components/ClickIncrease.js import withCounter from "./withCounter"; function ClickIncrease() { //...further code } export default withCounter(ClickIncrease); //ClickIncrease is now a wrapped component of the withCounter method.
یکی از ویژگیهای کلیدی یک کامپوننت Higher Order در React، قابلیت اشتراکگذاری props میان کامپوننتهای wrap شده است.
در فایل withCounter.js
، مقدار name
را به صورت prop به کامپوننت داخلی منتقل میکنیم:
// File: components/withCounter.js const UpdatedComponent = (OriginalComponent) => { function NewComponent(props) { return <OriginalComponent name="LogRocket" {...props} />; } return NewComponent; }; export default UpdatedComponent;
اکنون فایلهای HoverIncrease.js
و ClickIncrease.js
را به گونهای ویرایش میکنیم که مقدار prop جدید را نمایش دهند:
// File: components/HoverIncrease.js function HoverIncrease(props) { return ( <div> Value of 'name' in HoverIncrease: {props.name} </div> ); } export default withCounter(HoverIncrease); // File: components/ClickIncrease.js function ClickIncrease(props) { return ( <div> Value of 'name' in ClickIncrease: {props.name} </div> ); } export default withCounter(ClickIncrease);
همانطور که مشاهده میکنیم، با استفاده از HOCها میتوان به سادهترین شکل ممکن props مشترک را میان چندین کامپوننت به اشتراک گذاشت. این رویکرد در توسعه کامپوننتهای مقیاسپذیر و قابل استفاده مجدد، نقش کلیدی ایفا میکند.
مشابه props، ما میتوانیم state و توابع تغییر آن را هم از طریق HOC بین کامپوننتها به اشتراک بگذاریم. این کار باعث میشود تا منطق مشترک به صورت ماژولار و قابل استفاده مجدد نوشته شود.
در فایل components/withCounter.js
، یک HOC تعریف میکنیم که یک state counter
و یک تابع incrementCounter
را مدیریت میکند:
// File: components/withCounter.js import React, { useState } from 'react'; const withCounter = (OriginalComponent) => { function NewComponent(props) { const [counter, setCounter] = useState(10) // Initialize counter state return ( <OriginalComponent counter={counter} incrementCounter={() => setCounter(counter + 1)} {...props} /> ) } return NewComponent }; export default withCounter;
counter
برابر ۱۰
است.incrementCounter
با هر بار اجرا، مقدار counter
را به اندازه increaseCount
افزایش میدهد.کامپوننتهای HoverIncrease
و ClickIncrease
را برای استفاده از state و تابع مشترک تغییر میدهیم:
// File: components/HoverIncrease.js import withCounter from './withCounter' function HoverIncrease(props) { return ( <div onMouseOver={props.incrementCounter}> <p>Value of 'counter' in HoverIncrease: {props.counter}</p> </div> ) } export default withCounter(HoverIncrease) // File: components/ClickIncrease.js import withCounter from './withCounter' function ClickIncrease(props) { return ( <button onClick={props.incrementCounter}> Increment counter </button> <p>Value of 'counter' in ClickIncrease: {props.counter}</p> ) } export default withCounter(ClickIncrease)
با وجود اینکه HOCها امکان اشتراک منطق و state را فراهم میکنند، اما بین نسخههای مختلف یک کامپوننت wrap شده، state به اشتراک گذاشته نمیشود.
اگر نیاز به state سراسری در کل اپلیکیشن داریم، بهتر است از Context API استفاده کنیم.
اگرچه در حال حاضر کد ما به درستی عمل میکند، اما یک سناریو را در نظر میگیریم؛ اگر بخواهیم مقدار متغیر counter
را با یک عدد دلخواه افزایش دهیم، چه کاری باید انجام دهیم؟
خوشبختانه، با استفاده از HOCها، میتوانیم دادههای خاصی مانند یک مقدار عددی مشخص را نیز به کامپوننتهای child منتقل کنیم. این قابلیت از طریق ارسال پارامترها به تابع HOC فراهم میشود.
برای افزودن این قابلیت، ابتدا فایل components/withCounter.js
را به گونهای تغییر میدهیم که یک پارامتر جدید به نام increaseCount
را بپذیرد:
//This function will now accept an 'increaseCount' parameter. const UpdatedComponent = (OriginalComponent, increaseCount) => { function NewComponent(props) { return ( //this time, increment the 'size' variable by 'increaseCount' incrementCounter={() => setCounter((size) => size + increaseCount)} /> ); //further code..
در این قطعه کد، به React اطلاع دادهایم که تابع ما اکنون علاوه بر کامپوننت اصلی (OriginalComponent
)، یک پارامتر عددی به نام increaseCount
نیز دریافت خواهد کرد.
از این پارامتر برای تعیین میزان افزایش مقدار counter
استفاده میشود.
کامپوننتهای HoverIncrease
و ClickIncrease
را برای استفاده از این پارامتر، بهروزرسانی میکنیم:
//In HoverIncrease, change the 'export' statement: export default withCounter(HoverIncrease, 10); //value of increaseCount is 10. //this will increment the 'counter' Hook by 10. //In ClickIncrease: export default withCounter(ClickIncrease, 3); //value of increaseCount is 3. //will increment the 'counter' state by 3 steps.
با ارسال یک مقدار سفارشی (increaseCount
) به HOC، میتوانیم رفتار افزایش را در هر کامپوننت wrap شده به صورت داینامیک کنترل نماییم.
در نهایت، فایل withCounter.js
باید به شکل زیر باشد:
import React from "react"; import { useState } from "react"; const UpdatedComponent = (OriginalComponent, increaseCount) => { function NewComponent(props) { const [counter, setCounter] = useState(10); return ( name="LogRocket" counter={counter} incrementCounter={() => setCounter((size) => size + increaseCount)} /> ); } return NewComponent; }; export default UpdatedComponent;
فایل HoverIncrease.js
نیز باید به صورت زیر باشد:
import { useState } from "react"; import withCounter from "./withCounter"; function HoverIncrease(props) { const [fontSize, setFontSize] = useState(10); const { counter, incrementCounter } = props; return ( setFontSize((size) => size + 1)}> Increase on hover Size of font in onMouseOver function: {fontSize} Value of 'name' in HoverIncrease: {props.name} incrementCounter()}>Increment counter Value of 'counter' in HoverIncrease: {counter} ); } export default withCounter(HoverIncrease, 10);
و در نهایت، کامپوننت ClickIncrease
ما باید کد زیر را داشته باشد:
import { useEffect, useState } from "react"; import withCounter from "./withCounter"; function ClickIncrease(props) { const { counter, incrementCounter } = props; const [fontSize, setFontSize] = useState(10); return ( setFontSize((size) => size + 1)}> Increase with click Size of font in onClick function: {fontSize} Value of 'name' in ClickIncrease: {props.name} incrementCounter()}>Increment counter Value of 'counter' in ClickIncrease: {counter} ); } export default withCounter(ClickIncrease, 3);
انتخاب بین کامپوننت Higher Order و هوکها، بستگی به دو عامل کلیدی دارد:
از HOC استفاده میکنیم زمانی که نیاز داریم:
از هوک استفاده میکنیم زمانی که نیاز داریم:
در بسیاری از پروژههای امروزی، از ترکیب HOC و هوک برای ساختاردهی بهتر و انعطافپذیری بیشتر استفاده میشود.
در مثال زیر، از یک HOC با استفاده از یک هوک سفارشی (useAuth
) برای کنترل دسترسی به یک داشبورد ادمین استفاده شده است:
// Authentication HOC const withAuth = (WrappedComponent, requiredRole) => { return function AuthWrapper(props) { const { isAuthenticated, userRole } = useAuth(); // Custom hook for auth state const navigate = useNavigate(); useEffect(() => { if (!isAuthenticated) { navigate('/login'); } else if (requiredRole && userRole !== requiredRole) { navigate('/unauthorized'); } }, [isAuthenticated, userRole, navigate]); if (!isAuthenticated) { return null; // Optionally return a loader while determining authentication } return <WrappedComponent {...props} />; }; }; // Usage with a protected component const AdminDashboard = ({ data }) => { return <div>Admin Dashboard Content</div>; }; export default withAuth(AdminDashboard, 'admin');
در مثالهای پیچیدهتر، میتوانیم از هوکها برای بهینهسازی عملکرد یا مدیریت منطق خاص درون HOC استفاده کنیم. به عنوان مثال:
// Performance optimization HOC using hooks const withDataFetching = (WrappedComponent, fetchConfig) => { return function DataFetchingWrapper(props) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const { cache } = useCacheContext(); const { notify } = useNotification(); useEffect(() => { const fetchData = async () => { try { const cachedData = cache.get(fetchConfig.key); if (cachedData) { setData(cachedData); setLoading(false); return; } const response = await fetch(fetchConfig.url); const result = await response.json(); cache.set(fetchConfig.key, result); setData(result); } catch (err) { setError(err); notify({ type: 'error', message: 'Failed to fetch data', }); } finally { setLoading(false); } }; fetchData(); }, [fetchConfig.url, fetchConfig.key]); return <WrappedComponent {...props} data={data} loading={loading} error={error} />; }; };
اگر HOC ما شامل محاسبات سنگین یا زمانبر باشد، پیشنهاد میشود از تکنیکهایی مانند حافظهسازی (Memoization) برای جلوگیری از رندرهای غیرضروری استفاده نماییم.
در مثال زیر، از useMemo
و React.memo
برای بهینهسازی عملکرد بهره گرفتهایم:
// Assume expensiveDataProcessing is an expensive function that processes props.data const expensiveDataProcessing = (data) => { // ...expensive computations... return data; // Replace with the actual processed result }; const withOptimizedData = (WrappedComponent) => { function OptimizedDataWrapper(props) { const memoizedProps = useMemo(() => ({ ...props, processedData: expensiveDataProcessing(props.data), }), [props.data]); return <WrappedComponent {...memoizedProps} />; } return React.memo(OptimizedDataWrapper); }; export default withOptimizedData;
زمانی که نیاز داریم یک کامپوننت پایه را با چندین موضوع مشترک (مانند احراز هویت، دریافت داده، مدیریت خطا، و آنالیتیکس) توسعه دهیم، میتوانیم چندین HOC را ترکیب کنیم.
روش مستقیم ترکیب HOCها به شکل زیر میباشد:
const composedComponent = withAuth(withData(withLogging(BaseComponent)));
همچنین، برای ترکیب توابع از راست به چپ، میتوانیم از یک utility بهنام compose
استفاده کنیم (مشابه آنچه در کتابخانههایی مانند Redux وجود دارد):
// Utility const compose = (...functions) => x => functions.reduceRight((acc, fn) => fn(acc), x); // Usage const composedComponent = compose(withAuth, withData, withLogging)(BaseComponent);
// These will behave differently: const enhance1 = compose(withAuth, withDataFetching); const enhance2 = compose(withDataFetching, withAuth);
// Props flow through each HOC in the chain const withProps = compose( withAuth, // Adds isAuthenticated withDataFetching // Adds data, loading ); // Final component receives: { isAuthenticated, data, loading, ...originalProps }
استفاده بیش از حد از ترکیب HOCها میتواند به افزایش پیچیدگی در درخت کامپوننتها و کاهش عملکرد منجر شود.
const tooManyHOCs = compose( withAuth, withData, withLogging, withTheme, withTranslation, withRouter, withRedux ); // Each layer adds complexity and potential performance impact
روش بهتر این است که دغدعههای مرتبط را در یک HOC ترکیب نماییم:
const withDataFeatures = compose( withData, withLoading, withError ); const withAppFeatures = compose( withAuth, withAnalytics );
const withDebug = (WrappedComponent) => { return function DebugWrapper(props) { console.log('Component:', WrappedComponent.name); console.log('Props:', props); return <WrappedComponent {...props} />; }; }; const enhance = compose( withDebug, // Add at different positions to debug specific layers withAuth, withDebug, withDataFetching );
ترکیبهای قابل استفاده مجدد
const withDataProtection = compose( withAuth, withErrorBoundary, withLoading ); const withAnalytics = compose( withTracking, withMetrics, withLogging ); // Use them together or separately const EnhancedComponent = compose( withDataProtection, withAnalytics )(BaseComponent);
استفاده از تایپ اسکریپت در پیادهسازی HOCها، خوانایی و نگهداری کد را به شکل چشمگیری بهبود میبخشد:
import React, { useState, useEffect } from 'react'; interface WithDataProps<T> { data: T | null; loading: boolean; error: Error | null; } interface FetchConfig { url: string; } function withData<T, P extends object>( WrappedComponent: React.ComponentType<P & WithDataProps<T>>, fetchConfig: FetchConfig ): React.FC<P> { return function WithDataComponent(props: P) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { fetch(fetchConfig.url) .then((response) => response.json()) .then((result: T) => { setData(result); setLoading(false); }) .catch((err: Error) => { setError(err); setLoading(false); }); }, [fetchConfig.url]); return ( <WrappedComponent {...props} data={data} loading={loading} error={error} /> ); }; } export default withData;
یکی از نکات مهم در استفاده از HOCها، نحوه صحیح ارسال props به کامپوننت child است. فرایند ارسال props در HOCها کمی متفاوت از کامپوننتهای معمولی است.
برای مثال:
function App() { return ( {/*Pass in a 'secretWord' prop*/} ); } function HoverIncrease(props) { //read prop value: console.log("Value of secretWord: " + props.secretWord); //further code.. }
در این تئوری، باید در کنسول پیامی مشابه زیر مشاهده نماییم:
Value of secretWord: pineapple
اما در واقعیت، چنین چیزی چاپ نمیشود و Value of secretWord: undefined
را در کنسول مشاهده میکنیم. علت این است که prop با نام secretWord
به جای اینکه به کامپوننت HoverIncrease
برسد، به تابع withCounter
داده میشود و در ادامه به child منتقل نمیشود.
برای حل این مشکل، تنها کافی است تمام props ورودی را به کامپوننت اصلی منتقل کنیم:
const UpdatedComponent = (OriginalComponent, increaseCount) => { function NewComponent(props) { return ( //Pass down all incoming props to the HOC's children: {...props} /> ); } return NewComponent; };
این تغییر کوچک، مشکل را به طور کامل برطرف میکند.
در این مقاله، مفاهیم پایهای و پیشرفته مربوط به کامپوننتهای Higher Order در React را بررسی کردیم. HOCها ابزار قدرتمندی برای ساخت اپلیکیشنهای React با منطقهای قابلاستفاده مجدد هستند. با رعایت اصول بهینهسازی، ترکیب صحیح، و ساختاردهی حرفهای، میتوانیم از HOCها در کنار هوکها برای ایجاد معماریهای مدرن و مقیاسپذیر بهره ببریم.
دیدگاهها: