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