هشت ترفند مفید برای توسعه اپلیکیشن های React — راهنمای کاربردی
React تغییرات بسیاری را شاهد بوده است و هر بار کاربرانش را شگفتزده کرده است. ابتدا mixin-ها برای ایجاد و مدیریت اینترفیس معرفی شدند، سپس مفهوم کامپوننت کلاس معرفی شد و اینک قلابهای ریاکت روش ساخت اپلیکیشنها را متحول ساختهاند. در این مقاله در مورد هشت ترفند مفید برای توسعه اپلیکیشنهای React با شما صحبت خواهیم کرد.
دانستن این ترفندها به ساخت اپلیکیشنهای بهتر کمک میکند. شاید همه این موارد برای شما تازگی نداشته باشند، اما اگر حتی دست کم یکی از آنها برای شما مفید باشد، نباید فرصت مطالعه این مقاله را از دست بدهید.
1. ایجاد عناصر React با رشتهها
نخستین مورد در فهرست ترفندهای ریاکت به ایجاد یک عنصر معمولی DOM در ریاکت با استفاده از رشتههای ساده مربوط میشود. این رشتهها یک تگ عنصر HTML DOM را نمایش میدهند که به بیان دقیقتر رشتهای است که یک عنصر DOM را بازنمایی میکند.
برای نمونه میتوانید کامپوننتهای ریاکت را با انتساب رشته ‘div’ به یک متغیر به صورت زیر ایجاد کنید:
1import React from 'react'
2
3const MyComponent = 'div'
4
5function App() {
6 return (
7 <div>
8 <h1>Hello</h1>
9 <hr />
10 <MyComponent>
11 <h3>I am inside a {'<div />'} element</h3>
12 </MyComponent>
13 </div>
14 )
15}
ریاکت صرفاً React.createElement را فراخوانی میکند و از آن رشته برای ایجاد عنصر به صورت داخلی استفاده میکند.
این امکان به طور معمول در کتابخانههای کامپوننت مانند Material UI استفاده میشود که میتوانید یک prop به نام component اعلان کنید و فراخوانی کننده در مورد این که کدام گره ریشه کامپوننت میتواند به مقدار props.component تعیین شود، تصمیمگیری کند:
1function MyComponent({ component: Component = 'div', name, age, email }) {
2
3 return (
4 <Component>
5 <h1>Hi {name}</h1>
6 <div>
7 <h6>You are {age} years old</h6>
8 <small>Your email is {email}</small>
9 </div>
10 </Component>
11 )
12}
شیوه استفاده از آن به صورت زیر است:
1function App() {
2 return (
3 <div>
4 <MyComponent component="div" name="George" age={16} email="george@gmail.com">
5 </div>
6 )
7}
همچنین میتوانید کامپوننت سفارشی خود را در جایی که در گره ریشه استفاده خواهد شد ارسال کنید:
1function Dashboard({ children }) {
2 return (
3 <div style={{ padding: '25px 12px' }}>
4 {children}
5 </div>
6 )
7}
8
9function App() {
10 return (
11 <div>
12 <MyComponent component={Dashboard} name="George" age={16} email="george@gmail.com">
13 </div>
14 )
15}
2. استفاده از کرانهای خطا
ما در جاوا اسکریپت به طور معمول اغلب خطاها را در بخش اجرای کد با try/catch مدیریت میکنیم. در این بلوک کد میتوانیم خطاهایی را که رخ میدهند catch کنیم. هنگامی که این خطاها در بلوک catch گیر میافتند، میتوانیم جلوی از کار افتادن اپلیکیشن را درون کرانهای کد بگیریم. نمونهای از این حالت را در ادامه میبینید:
1function getFromLocalStorage(key, value) {
2 try {
3 const data = window.localStorage.get(key)
4 return JSON.parse(data)
5 } catch (error) {
6 console.error
7 }
8}
ریاکت در نهایت چیزی به جز جاوا اسکریپت نیست و از این رو بسیاری بر این باورند که میتوانیم خطاها را با استفاده از همین راهبرد گیر انداخته و مدیریت کنیم. با این حال به دلیل ماهیت ریاکت، خطاهای جاوا اسکریپت درون کامپوننتها حالت درونی ریاکت را از بین میبرند و موجب بروز خطاهای emit cryptic در رندرهای آتی میشوند.
به همین جهت تیم ریاکت مفهوم «کرانهای خطا» (error boundaries) را معرفی کرده است که هر توسعهدهنده ریاکت باید با آنها آشنا باشد و از آنها در اپلیکیشنهای ریاکت خود استفاده کند. مشکل در مورد خطاهایی که پیش از معرفی مفهوم «کرانهای خطا» رخ میداد، این بود که وقتی خطاهای cryptic در رندرهای آتی و پس از رخ دادن در رندهای پیشین صادر میشدند، ریاکت روشی برای مدیریت و بازیابی از این وضعت در کامپوننتها ارائه نمیکرد. این همان جایی بود که به مفهوم کرانهای خطا نیاز داشتیم.
کرانهای خطا کامپوننتهای ریاکت هستند که خطاها را هر جایی در درخت کامپوننت به دام میاندازند، آنها را لاگ میکنند و میتوانند به جای درخت کامپوننت که از کار افتاده است، UI پشتیبان (fallback) را نمایش دهند.
کرانهای خطا، اقدام به یافتن خطاها در زمان رندر کردن، درون متدهای چرخه عمری و درون سازندههای کل درخت زیر خودشان میکنند. به همین دلیل است که آنها را در ابتدای اپلیکیشن اعلان و رندر میکنیم. در ادامه مثالی از مستندات ریاکت را مشاهده میکنید:
1class ErrorBoundary extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { hasError: false }
5 }
6
7 static getDerivedStateFromError(error) {
8 // Update state so the next render will show the fallback UI.
9 return { hasError: true }
10 }
11
12 componentDidCatch(error, errorInfo) {
13 // You can also log the error to an error reporting service
14 logErrorToMyService(error, errorInfo)
15 }
16
17 render() {
18 if (this.state.hasError) {
19 // You can render any custom fallback UI
20 return <h1>Something went wrong.</h1>
21 }
22 return this.props.children
23 }
24}
میتوانید از آن به عنوان یک کامپوننت معمولی استفاده کنید:
1<ErrorBoundary>
2 <MyWidget />
3</ErrorBoundary>
3. حفظ مقادیر پیشین
در زمان بهروزرسانی props یا حالت میتوان مقادیر پیشین را صرفاً با استفاده از React.useRef حفظ کرد. برای نمونه برای ردگیری تغییرات کنونی و قبلی آیتمهای یک ارائه میتوان یک React.useRef ایجاد کرد که به مقدار قبلی را انتساب یابد و یک React.useRef نیز برای مقدار کنونی تعریف کنیم:
1function MyComponent() {
2 const [names, setNames] = React.useState(['bob'])
3 const prevNamesRef = React.useRef([])
4
5 React.useEffect(() => {
6 prevNamesRef.current = names
7 })
8
9 const prevNames = prevNamesRef.current
10
11 return (
12 <div>
13 <h4>Current names:</h4>
14 <ul>
15 {names.map((name) => (
16 <li key={name}>{name}</li>
17 ))}
18 </ul>
19 <h4>Previous names:</h4>
20 <ul>
21 {prevNames.map((prevName) => (
22 <li key={prevName}>{prevName}</li>
23 ))}
24 </ul>
25 </div>
26 )
27}
این کد به این دلیل کار میکند که React.useEffect پس از پایان یافتن رندرینگ کامپوننتها اجرا میشود. هنگامی که setNames فراخوانی میشود، کامپوننت مجدداً رندر میشود و prefNamesRef نامهای پیشین را نگهداری میکند، زیرا React.useEffect آخرین کدی است که از رندر قبلی اجرا شده است. و از آنجا که prevNamesRef.current را در useEffect مجدداً انتساب دادیم، به نامهای پیشین در مرحله بعدی رندر تبدیل میشود، زیرا نامهای برجایمانده از مرحله قبلی رندر را انتساب میدهد.
4. استفاده از React.useRef برای بررسیهای غیر انعطافپذیر
تا پیش از معرفی قلابهای ریاکت یک متد استاتیک به نام componentDidMount در کامپوننتهای کلاس وجود داشت که میتوانستیم از این که عملیاتی مانند واکشی دادهها پس از نصب شدن کامپوننت روی DOM اجرا میشوند، مطمئن باشیم.
اما پس از معرفی، قلابهای ریاکت به سرعت به روشی محبوب برای نوشتن کامپوننتها به جای کامپوننتهای کلاسی تبدیل شدند. هنگامی که میخواهیم بررسی کنیم آیا یک کامپوننت Mount شده یا نه و از تغییر حالت پس از Unmount شدن کامپوننت جلوگیری کنیم، میتوانیم به صورت زیر عمل کنیم:
1function MyComponent() {
2 const [names, setNames] = React.useState(['bob'])
3 const prevNamesRef = React.useRef([])
4
5 React.useEffect(() => {
6 prevNamesRef.current = names
7 })
8
9 const prevNames = prevNamesRef.current
10
11 return (
12 <div>
13 <h4>Current names:</h4>
14 <ul>
15 {names.map((name) => (
16 <li key={name}>{name}</li>
17 ))}
18 </ul>
19 <h4>Previous names:</h4>
20 <ul>
21 {prevNames.map((prevName) => (
22 <li key={prevName}>{prevName}</li>
23 ))}
24 </ul>
25 </div>
26 )
27}
قلابها پس از مهاجرت به React Hooks دیگر componentDidMount ندارند و مفهوم نشت حافظه از بهروزرسانی حالت پس از unmount شدن کامپوننت همچنان در مورد Hooks رخ میدهد.
با این حال روش مشابه componentDidMount برای استفاده از قلابهای ریاکت، بهرهگیری از React.useEffect است، زیرا پس از این رندرینگ کامپوننتها پایان یافت اجرا میشود. اگر از React.useRef برای انتساب مقدار به مقدار mount-شده استفاده کنید، میتوانید همان نتیجه مثال کامپوننت کلاسی را به دست آورید:
1import React from 'react'
2import axios from 'axios'
3
4function MyComponent() {
5 const [frogs, setFrogs] = React.useState([])
6 const [error, setError] = React.useState(null)
7 const mounted = React.useRef(false)
8
9 async function fetchFrogs(params) {
10 try {
11 const response = await axios.get('https://some-frogs-api.com/v1/', {
12 params,
13 })
14 if (mounted.current) {
15 setFrogs(response.data.items)
16 }
17 } catch (error) {
18 if (mounted.current) {
19 setError(error)
20 }
21 }
22 }
23
24 React.useEffect(() => {
25 mounted.current = true
26 return function cleanup() {
27 mounted.current = false
28 }
29 }, [])
30
31 return (
32 <div>
33 <h4>Frogs:</h4>
34 <ul>
35 {this.state.frogs.map((frog) => (
36 <li key={frog.name}>{frog.name}</li>
37 ))}
38 </ul>
39 </div>
40 )
41}
مثال دیگری از استفاده خوب از این حالت، ردگیری آخرین تغییرات بدون ایجاد رندر مجدد است و با استفاده همزمان با React.useMemo به دست میآید:
1function setRef(ref, value) {
2 // Using function callback version
3 if (typeof ref === 'function') {
4 ref(value)
5 // Using the React.useRef() version
6 } else if (ref) {
7 ref.current = value
8 }
9}
10
11function useForkRef(refA, refB) {
12 return React.useMemo(() => {
13 if (refA == null && refB == null) {
14 return null
15 }
16 return (refValue) => {
17 setRef(refA, refValue)
18 setRef(refB, refValue)
19 }
20 }, [refA, refB])
21}
بدین ترتیب در صورتی که ref props تغییر یافته و تعریف شده باشد، تابع جدیدی ایجاد خواهد شد. این بدان معنی است که ریاکت، ref فورک شده قدیمی را با null فراخوانی میکند و ref فورک شده جدید با ref جاری فراخوانی میشود. از آنجا که از React.useMemo استفاده میکنیم، ref-ها تا زمانی که prop-های مربوطهی refA یا refB تغییر یابند، در حافظه میمانند و در طی این فرایند پاکسازی طبیعی رخ میدهد.
5. سفارشیسازی عناصر وابسته به عناصر دیگر با React.useRef
React.useRef کاربردهای مفید مختلفی دارد که شامل انتساب خودش به ref prop در گرههای ریاکت میشود:
1function MyComponent() {
2 const [position, setPosition] = React.useState({ x: 0, y: 0 })
3 const nodeRef = React.useRef()
4
5 React.useEffect(() => {
6 const pos = nodeRef.current.getBoundingClientRect()
7 setPosition({
8 x: pos.x,
9 y: pos.y,
10 })
11 }, [])
12
13 return (
14 <div ref={nodeRef}>
15 <h2>Hello</h2>
16 </div>
17 )
18}
اگر بخواهیم موقعیت مختصات عنصر div را به دست آوریم، این مثال کافی است. با این حال اگر عنصر دیگر، جایی در اپلیکیشن بخواهد موقعیتهای خود را در آن زمان که position تغییر مییابد یا نوعی منطق مختصات اعمال میشود، بهروزرسانی کند، بهترین روش این است که این کار را با استفاده از ref callback function pattern انجام دهیم.
زمانی که از الگوی تابع callback استفاده میکنیم، یا یک وهله از کامپوننت ریاکت به دست میآید یا عنصر HTML DOM به عنوان آرگومان نخست بازگشت مییابد. مثال زیر صرفاً یک نمونه ساده از حالتی را نشان میدهد که setRef تابع callback اعمال شده روی یک ref prop است. چنان که میبینید درون setRef امکان انجام هر کاری وجود دارد و این وضعیت عکس بهکارگیری مستقیم نسخه React.useRef روی عنصر DOM است:
1const SomeComponent = function({ nodeRef }) {
2 const ownRef = React.useRef()
3
4 function setRef(e) {
5 if (e && nodeRef.current) {
6 const codeElementBounds = nodeRef.current.getBoundingClientRect()
7 // Log the <pre> element's position + size
8 console.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`)
9 ownRef.current = e
10 }
11 }
12
13 return (
14 <div
15 ref={setRef}
16 style={{ width: '100%', height: 100, background: 'green' }}
17 />
18 )
19}
20
21function App() {
22 const [items, setItems] = React.useState([])
23 const nodeRef = React.useRef()
24
25 const addItems = React.useCallback(() => {
26 const itemNum = items.length
27 setItems((prevItems) => [
28 ...prevItems,
29 {
30 [`item${itemNum}`]: `I am item # ${itemNum}'`,
31 },
32 ])
33 }, [items, setItems])
34
35 return (
36 <div style={{ border: '1px solid teal', width: 500, margin: 'auto' }}>
37 <button type="button" onClick={addItems}>
38 Add Item
39 </button>
40 <SomeComponent nodeRef={nodeRef} />
41 <div ref={nodeRef}>
42 <pre>
43 <code>{JSON.stringify(items, null, 2)}</code>
44 </pre>
45 </div>
46 </div>
47 )
48}
6. کامپوننتهای مرتبه بالاتر
یک الگوی رایج در جاوا اسکریپت ساده، ایجاد تابعهای قدرتمند با قابلیت استفاده مجدد به صورت «تابعهای مرتبه بالاتر» (higher-order function) است. از آنجا که React در نهایت خود همان جاوا اسکریپت است، میتوان از تابعهای مرتبه بالاتر درون ریاکت نیز استفاده کرد.
در مورد کامپوننتهای با قابلیت استفاده مجدد، این ترفند به صوت استفاده از «کامپوننتهای مرتبه بالاتر (higher-order components) است. یک کامپوننت مرتبه بالاتر، تابعی است که یک کامپوننت به عنوان آرگومان میگیرد و یک کامپوننت بازگشت میدهد.
همان طور که میتوان از تابعهای مرتبه بالاتر برای جداسازی منطق و اشتراک آن میان تابعهای دیگر در اپلیکیشن استفاده کرد، کامپوننتهای مرتبه بالاتر نیز به ما امکان میدهند که منطق را از کامپوننتها جدا کنیم و آن را در میان کامپوننتهای دیگر به اشتراک بگذاریم.
در ادامه مثالی از یک کامپوننت مرتبه بالاتر معرفی شده است. در این قطعه کد یک کامپوننت مرتبه بالاتر به نام withBorder یک کامپوننت سفارشی میگیرد و یک کامپوننت «لایه میانی» (middle layer) بازگشت میدهد. سپس هنگامی که والد تصمیم میگیرد تا این کامپوننت مرتبه بالاتر را که بازگشت یافته است رندر کند، یک کامپوننت را فراخوانی کرده و props را که از کامپوننت لایه میانی به آن ارسال شده، دریافت میکند:
1import React from 'react'
2
3// Higher order component
4const withBorder = (Component, customStyle) => {
5 class WithBorder extends React.Component {
6 render() {
7 const style = {
8 border: this.props.customStyle ? this.props.customStyle.border : '3px solid teal'
9 }
10 return <Component style={style} {...this.props} />
11 }
12 }
13
14 return WithBorder
15}
16
17function MyComponent({ style, ...rest }) {
18 return (
19 <div style={style} {...rest}>
20 <h2>
21 This is my component and I am expecting some styles.
22 </h2>
23 </div>
24 )
25}
26
27export default withBorder(MyComponent, {
28 border: '4px solid teal'
29})
7. رندر کردن props
یکی از ترفندهای خوب که در کتابخانه ریاکت استفاده میشود، الگوی رندر prop است. این الگو مشابه کامپوننتهای مرتبه بالاتر است، چون مسئله مشابهی را به صورت اشتراک کد بین کامپوننتهای مختلف حل میکند. رندر props یک تابع عرضه میکند که هدف آن ارسال همه چیزهایی است که دنیای بیرون برای رندر کردن فرزندانش نیاز دارد. مقدماتیترین روش برای رندر کامپوننتها در ریاکت به صورت زیر است:
1function MyComponent() {
2 return <p>My component</p>
3}
4
5function App() {
6 const [fetching, setFetching] = React.useState(false)
7 const [fetched, setFetched] = React.useState(false)
8 const [fetchError, setFetchError] = React.useState(null)
9 const [frogs, setFrogs] = React.useState([])
10
11 React.useEffect(() => {
12 setFetching(true)
13
14 api.fetchFrogs({ limit: 1000 })
15 .then((result) => {
16 setFrogs(result.data.items)
17 setFetched(true)
18 setFetching(false)
19 })
20 .catch((error) => {
21 setError(error)
22 setFetching(false)
23 })
24
25 }, [])
26
27 return <MyComponent fetching={fetching} fetched={fetched} fetchError={fetchError} frogs={frogs} />
28}
هنگام استفاده از رندر کردن props، آن prop که فرزندانش را رندر میکند بنا به عرف، render نامگذاری میشود:
1function MyComponent({ render }) {
2 const [fetching, setFetching] = React.useState(false)
3 const [fetched, setFetched] = React.useState(false)
4 const [fetchError, setFetchError] = React.useState(null)
5 const [frogs, setFrogs] = React.useState([])
6
7 React.useEffect(() => {
8 setFetching(true)
9
10 api.fetchFrogs({ limit: 1000 })
11 .then((result) => {
12 setFrogs(result.data.items)
13 setFetched(true)
14 setFetching(false)
15 })
16 .catch((error) => {
17 setError(error)
18 setFetching(false)
19 })
20 }, [])
21
22 return render({
23 fetching,
24 fetched,
25 fetchError,
26 frogs,
27 })
28}
در این مثال MyComponent نمونهای از یک کامپوننت است. آن را render prop component مینامیم، زیرا render را به عنوان یک prop افشا کرده و آن را برای رندر کردن فرزندانش فراخوانی میکند.
این الگوی قدرتمندی در ریاکت محسوب میشود، زیرا به ما امکان ارسال حالت و داده های اشتراکی را از طریق callback رندر و آرگومانها فراهم میکند. بدین ترتیب کامپوننت میتواند در چندین کامپوننت مختلف رندر شده و مورد استفاده مجدد قرار گیرد:
1function App() {
2 return (
3 <MyComponent
4 render={({
5 fetching,
6 fetched,
7 fetchError,
8 frogs
9 }) => (
10 <div>
11 {fetching ? 'Fetching frogs...' : fetched ? 'The frogs have been fetched!' : fetchError ? `An error occurred while fetching the list of frogs: ${fetchError.message}` : null}
12 <hr />
13 <ul style={{
14 padding: 12,
15 }}>
16 {frogs.map((frog) => (
17 <li key={frog.name}>
18 <div>
19 Frog's name: {frog.name}
20 </div>
21 <div>
22 Frog's age: {frog.age}
23 </div>
24 <div>
25 Frog's gender: {frog.gender}
26 </div>
27 </li>
28 ))}
29 </ul>
30 </div>
31 )}
32 />
33 )
34}
8. Memoize
یکی از مهمترین نکتههایی که هر توسعهدهنده ریاکت باید بداند، شیوه بهینهسازی عملکرد کامپوننتهایی مانند React.memo است. این کار میتواند به جلوگیری از برخی خطاهای بد، مانند حلقههای نامتناهی که موجب از کار افتادن اپلیکیشن در زمان اجرا میشود کمک کند. برای آشنایی با این مفهوم پیشنهاد میکنیم این صفحههای مستندات ریاکت را مطالعه کنید:
بدین ترتیب به پایان این مقاله میرسیم. امیدواریم از مطالعه آن بهره آمورشی لازم را برده باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش ساخت پروژه با فریمورک React Native
- ری اکت (React) — راهنمای جامع برای شروع به کار
- آشنایی با پورتال React و کاربرد آن — راهنمای مقدماتی
==