ایجاد منوی سایدبار دینامیک به روش بازگشتی در React — از صفر تا صد
سایدبارها در صفحههای وب به دلیل کارکرد ناوبری که ارائه میکنند، یکی از مفیدترین اجزای موجود در صفحه محسوب میشوند. در این مقاله یک منوی سایدبار دینامیک مدرن به روش بازگشتی در ریاکت ایجاد میکنیم. روش بازگشتی (Recursion) تکنیکی است که به تابع امکان فراخوانی خودش را به صورت مکرر تا زمانی که شرط خاصی برقرار شود میدهد. هنگام استفاده از تکنیک بازگشتی در این مقاله از سه قاعده بازگشت، به شرح زیر بهره گرفته شده است:
- تابع باید شرطی داشته باشد که خودش را تخریب کند.
- تابع باید یک شرط مبنا داشته باشد.
- تابع باید خودش را فراخوانی کند.
سایدبارها در عمل جزئی ضروری از صفحه وب محسوب میشوند، هر چند در وهله اول توجه ما را جلب نکنند. دلیل این امر آن است که به کاربران کمک میکنند تا به روشهای مختلف در وبسایت حرکت کنند. به این ترتیب برای نمونه کاربران میتوانند به جای منوی ناوبری منطقی، از ناوبری محتوایی استفاده کنند.
اما شاید از خود بپرسید، چرا باید از تکنیک بازگشتی روی سایدبارها استفاده کنیم؟ این روش چه تفاوتی با نوشتن آیتمهای سایدبار به صورت دستی دارد؟ اگر کمی در اینترنت به گشتوگذار بپردازید، احتمالاً با سایدبار یک وبسایت مواجه میشوید که متوجه میشود آیتمهای سایدبار در واقع بخشهای فرعی وبسایت هستند. برخی سایتها سایدبارهایی دارند که آیتمهای خاصی را بر مبنای مسیر صفحهای که کاربر مراجعه کرده است، نمایش داده یا پنهان میکنند. این روش قدرتمندی است.
برای نمونه اگر به تصویر زیر نگاه کنید، درون دایره قرمز بخش ویرایشگران را به عنوان یکی از آیتمهای سایدبار و سه آیتم دیگر (Code Editor ،Markdown ،Text Editor) زیر آن را میبینید که بخشهای فرعی آن محسوب میشوند:
در انتهای این مقاله خواهیم دید که این سایدبار به ظاهر پیچیده با کمتر از 50 خط کد نوشته شده است. در ادامه مثالی از شیوه بسط کامپوننت سایدبار این مقاله به یک سایدبار شیکتر و در عین حال حفظ ظاهر تمیز آن را میبینید:
اینک بدون هر گونه توضیح اضافی شیوه انجام کار را میبینم. در این راهنما قصد داریم یک پروژه ریاکت را به سرعت با استفاده از اسکریپت create-react-app (+) ایجاد کنیم. در ادامه یک پروژه با استفاده از دستور زیر ایجاد میکنیم. در این راهنما نام پروژه خود را modern-sidebar (+) میگذاریم:
npx create-react-app modern-sidebar
اینک به دایرکتوری مربوطه میرویم:
cd modern-sidebar
در ادامه درون مدخل اصلی فایل src/index.js کمی تمیزکاری میکنیم تا بتوانیم روی خود کامپوننت متمرکز شویم:
1import React from 'react'
2import ReactDOM from 'react-dom'
3import App from './App'
4import './styles.css'
5import * as serviceWorker from './serviceWorker'
6ReactDOM.render(<App />, document.getElementById('root'))
7serviceWorker.unregister()
اکنون فایلی به نام src/App.js ایجاد میکنیم:
1import React from 'react'
2const App = () => <div />
3export default App
کلاس App، کامپوننت Sidebar ما را با ساخت فایل Sidebar.js ایمپورت کرده و مورد استفاده قرار میدهد. بنابراین این فایل را ایجاد میکنیم:
1import React from 'react'
2function Sidebar() {
3 return null
4}
5export default Sidebar
اکنون قصد داریم یک کتابخانه CSS نصب کنیم، اما میتوانیم همین کارکرد سایدبار را بدون استفاده از آن نیز به دست آوریم. دلیل استفاده از این کتابخانه آن است که از جلوههای اضافی همراه با آیکونهای آماده آن استفاده کنیم.
npm install @material-ui/core @material-ui/icons
زمانی که کتابخانه فوق نصب شد، باید در مورد ساختار مبنای رابط کاربری که سایدبار خود را بر مبنhی آن خواهیم ساخت تأمل کنیم. یک راهحل این است که از عنصر لیست نامرتب (<ul>) استفاده کنیم که آیتمهای لیست (<li>) را رندر میکند. به این منظور List و ListItem را از material-ui/core@ ایمپورت میکنیم زیرا کامپوننت List اساساً یک عنصر ul است و کامپوننت ListItem نیز اساساً یک li است.
در ادامه اقدام به هاردکد کردن برخی آیتمها در سایدبار میکنیم تا شیوه نمایش ظاهری آن را دیده و اعتمادبهنفس خود را بالاتر ببریم. برخی اوقات کمی اعتمادبهنفس بیشتر میتواند به افزایش بهرهوری کمک کند:
فایل Sidebar.js
1import React from 'react'
2import List from '@material-ui/core/List'
3import ListItem from '@material-ui/core/ListItem'
4import ListItemText from '@material-ui/core/ListItemText'
5
6function Sidebar() {
7 return (
8 <List disablePadding dense>
9 <ListItem button>
10 <ListItemText>Home</ListItemText>
11 </ListItem>
12 <ListItem button>
13 <ListItemText>Billing</ListItemText>
14 </ListItem>
15 <ListItem button>
16 <ListItemText>Settings</ListItemText>
17 </ListItem>
18 </List>
19 )
20}
21
22export default Sidebar
از disablePadding و dense برای کاهش اندکی در اندازه هر یک از آیتمها استفاده شود و یک prop به نام button برای افزودن جلوه ripple مورد استفاده قرار گرفته است. تاکنون به این نتیجه دست یافتهایم:
اکنون که اعتمادبهنفس خود را بالا بردهایم، نوبت آن رسیده است که props.items را تعریف کنیم که Sidebar برای رندر آیتمهایش به آن وابسته است. معنی حرف فوق این است که باید یک prop به نام items داشته باشیم که شامل آرایهای از اشیا برای نمایش هر آیتم در منوی سایدبار باشد. ما میخواهیم کاربرد را تا حد امکان، ساده حفظ کنیم چون در غیر این صورت کامپوننت به سرعت پیچیدهتر از حد قبل کنترل میشود. ابتدا آیتمها را در کامپوننت App ایجاد کرده و آن را به صورت props.items به Sidebar ارسال میکنیم:
فایل App.js
1import React from 'react'
2import Sidebar from './Sidebar'
3
4const items = [
5 { name: 'home', label: 'Home' },
6 { name: 'billing', label: 'Billing' },
7 { name: 'settings', label: 'Settings' },
8]
9
10function App() {
11 return (
12 <div>
13 <Sidebar items={items} />
14 </div>
15 )
16}
17
18export default App
اکنون کامپوننت Sidebar را بهروز میکنیم تا ساختار این آرایه را نشان دهیم.
فایل Sidebar.js
1import React from 'react'
2import List from '@material-ui/core/List'
3import ListItem from '@material-ui/core/ListItem'
4import ListItemText from '@material-ui/core/ListItemText'
5
6function Sidebar({ items }) {
7 return (
8 <List disablePadding dense>
9 {items.map(({ label, name, ...rest }) => (
10 <ListItem key={name} button {...rest}>
11 <ListItemText>{label}</ListItemText>
12 </ListItem>
13 ))}
14 </List>
15 )
16}
17
18export default Sidebar
نکتهای که شاید متوجه شده باشید، این است که سایدبار ما بیش از حد بزرگ است. سایدبارها به طور معمول یک بخش از صفحه را اشغال میکنند. از این رو قصد داریم اندازه آن را تا حد مطلوب کاهش دهیم. در ادامه مقدار max-width را روی 200px تنظیم میکنیم. از این رو باید یک عنصر div ایجاد کنیم که کامپوننت List را درون آن قرار دهیم.
دلیل این که به جای اعمال مستقیم استایلها روی کامپوننت List، عنصر div دیگری ایجاد میکنیم این است که نمیخواهیم List مسئول اندازه عرض باشد. بدین ترتیب در آینده میتوانیم List را در یک کامپوننت سایدبار با قابلیت استفاده مجدد انتزاع کنیم که با هر اندازهای بسته به عنصر والد قابلیت تطبیق دارد. کامپوننت Sidebar.js اینک به صورت زیر در آمده است:
1import React from 'react'
2import List from '@material-ui/core/List'
3import ListItem from '@material-ui/core/ListItem'
4import ListItemText from '@material-ui/core/ListItemText'
5
6function Sidebar({ items }) {
7 return (
8 <div className="sidebar">
9 <List disablePadding dense>
10 {items.map(({ label, name, ...rest }) => (
11 <ListItem key={name} button {...rest}>
12 <ListItemText>{label}</ListItemText>
13 </ListItem>
14 ))}
15 </List>
16 </div>
17 )
18}
19
20export default Sidebar
21
در ادامه درون فایل index.css استایلهای CSS را برای کلاس Sidebar تعریف میکنیم:
1.sidebar {
2 max-width: 240px;
3 border: 1px solid rgba(0, 0, 0, 0.1);
4}
Material-UI مستقیماً از سازوکار استایلبندی CSS (+) خود با استفاده از رویکرد CSS-in-JS بهره میگیرد، اما در این مقاله از CSS معمولی استفاده میکنیم تا موارد مختلف به صورت غیر لازمی پیچیده نشوند.
اینک میتوانیم آن را به همین صورت ابتدایی رها کنیم و در آینده آن را فراخوانی نماییم. با این حال از آیتمهای فرعی پشتیبانی نمیکند. ما میخواهیم بتوانیم روی یک آیتم سایدبار کلیک کنیم و در صورتی که آیتم فرعی داشته باشد، آنها باز شوند. داشتن آیتم فرعی به سازماندهی سایدبار با گروهبندی آیتمهای اضافی درون یک سایدبار دیگر کمک میکند:
روشی که برای پشتیبانی از این قابلیت استفاده خواهیم کرد این است که یک گزینه درون هر آیتم سایدبار قرار میدهیم تا کامپوننت بتواند آیتمهای فرعی آن را تشخیص دهد. آرایه آیتمها در کامپوننت App را طوری تغییر میدهیم که آیتمهای فرعی را ارسال کنیم:
1import React from 'react'
2import Sidebar from './Sidebar'
3
4const items = [
5 { name: 'home', label: 'Home' },
6 {
7 name: 'billing',
8 label: 'Billing',
9 items: [
10 { name: 'statements', label: 'Statements' },
11 { name: 'reports', label: 'Reports' },
12 ],
13 },
14 {
15 name: 'settings',
16 label: 'Settings',
17 items: [{ name: 'profile', label: 'Profile' }],
18 },
19]
20
21function App() {
22 return (
23 <div>
24 <Sidebar items={items} />
25 </div>
26 )
27}
28
29export default App
برای این که بتوانید آیتمهای فعلی یک آیتم سایدبار را رندر کنید، باید مشخصه items را در زمان رندر کردن آیتمهای سایدبار زیر نظر بگیرید:
1function Sidebar({ items }) {
2 return (
3 <div className="sidebar">
4 <List disablePadding dense>
5 {items.map(({ label, name, items: subItems, ...rest }) => (
6 <ListItem style={{ paddingLeft: 18 }} key={name} button {...rest}>
7 <ListItemText>{label}</ListItemText>
8 {Array.isArray(subItems) ? (
9 <List disablePadding>
10 {subItems.map((subItem) => (
11 <ListItem key={subItem.name} button>
12 <ListItemText className="sidebar-item-text">
13 {subItem.label}
14 </ListItemText>
15 </ListItem>
16 ))}
17 </List>
18 ) : null}
19 </ListItem>
20 ))}
21 </List>
22 </div>
23 )
24}
چنان که در تصویر زیر میبینید، کامپوننت سایدبار به هم ریخته است:
این آن سایدباری نبود که ما میخواستیم به آن دست پیدا کنیم. اکنون از آنجا که نمیخواهیم کاربران وبسایتمان هنگام مراجعه به وبسایت ما دکمه بستن را زده و دیگر هرگز به آن بازنگردند، باید روشی برای بهتر ساختن ظاهر وبسایت هم برای چشمها و همچنین DOM وبسایت پیدا کنیم.
شاید بپرسید منظور از DOM چیست؟ اگر دقیقتر نگاه کنید مشکلی وجود دارد. اگر کاربر روی یک آیتم فرعی کلیک کند، آیتم والد، آیتم فرعی را رندر میکند و «دستگیره کلیک» (Click Handler) کاربر را نیز مصرف میکند، چون آنها با هم همپوشانی دارند. این وضعیت بدی است و مشکلات غیرمنتظرهای برای تجربه کاربر رقم میزند. اینک به جداسازی والد از فرزندانش (آیتمهای فرعی) نیاز داریم، به طوری که آیتمهای فرعی خود را به صورت همزمان رندر کنند و بدین ترتب رویدادهای ماوس با هم تصادم نداشته باشند:
1function Sidebar({ items }) {
2 return (
3 <div className="sidebar">
4 <List disablePadding dense>
5 {items.map(({ label, name, items: subItems, ...rest }) => (
6 <React.Fragment key={name}>
7 <ListItem style={{ paddingLeft: 18 }} button {...rest}>
8 <ListItemText>{label}</ListItemText>
9 </ListItem>
10 {Array.isArray(subItems) ? (
11 <List disablePadding>
12 {subItems.map((subItem) => (
13 <ListItem key={subItem.name} button>
14 <ListItemText className="sidebar-item-text">
15 {subItem.label}
16 </ListItemText>
17 </ListItem>
18 ))}
19 </List>
20 ) : null}
21 </React.Fragment>
22 ))}
23 </List>
24 </div>
25 )
26}
اکنون تقریباً همه چیز به حالت عادی بازگشته است:
با این حال در تصویر فوق به نظر میرسد که با مشکل جدیدی مواجه شدهایم. مشکل جدید این است که آیتمهای فرعی به طرز شدیدی بزرگتر از آیتمهای سطح بالا هستند. ما باید روشی برای تشخیص این که کدام موارد آیتم فرعی و کدام یک آیتمهای سطح بالا هستند، داشته باشیم. میتوانیم آن را به روش هاردکد بنویسیم.
فایل Sidebar.js
1function Sidebar({ items }) {
2 return (
3 <div className="sidebar">
4 <List disablePadding dense>
5 {items.map(({ label, name, items: subItems, ...rest }) => {
6 return (
7 <React.Fragment key={name}>
8 <ListItem style={{ paddingLeft: 18 }} button {...rest}>
9 <ListItemText>{label}</ListItemText>
10 </ListItem>
11 {Array.isArray(subItems) ? (
12 <List disablePadding dense>
13 {subItems.map((subItem) => {
14 return (
15 <ListItem
16 key={subItem.name}
17 style={{ paddingLeft: 36 }}
18 button
19 dense
20 >
21 <ListItemText>
22 <span className="sidebar-subitem-text">
23 {subItem.label}
24 </span>
25 </ListItemText>
26 </ListItem>
27 )
28 })}
29 </List>
30 ) : null}
31 </React.Fragment>
32 )
33 })}
34 </List>
35 </div>
36 )
37}
فایل styles.css
1.sidebar-subitem-text {
2 font-size: 0.8rem;
3}
اما میدانیم که کامپوننت سایدبار ما باید دینامیک باشد. به طور معمول میخواهیم آیتمهای سایدبار بر اساس آیتمهای ارسالی در props از سوی فراخواننده ایجاد شوند. به این منظور قصد داریم از یک prop ساده به نام depth استفاده کنیم که آیتمهای سایدبار مورد بهرهبرداری قرار میدهند و بر مبنای آن، عمقی که دارند را تنظیم میکنند و دیگر مهم نیست که چه قدر در درخت سایدبار فرو میرویم. همچنین باید آیتم سایدبار را در کامپوننت خودش استخراج کنیم به طوری که بتوانیم عمق را بدون درگیر شدن با منطق حالت آغازین افزایش دهیم. کد به صورت زیر است.
فایل Sidebar.js
1function SidebarItem({ label, items, depthStep = 10, depth = 0, ...rest }) {
2 return (
3 <>
4 <ListItem button dense {...rest}>
5 <ListItemText style={{ paddingLeft: depth * depthStep }}>
6 <span>{label}</span>
7 </ListItemText>
8 </ListItem>
9 {Array.isArray(items) ? (
10 <List disablePadding dense>
11 {items.map((subItem) => (
12 <SidebarItem
13 key={subItem.name}
14 depth={depth + 1}
15 depthStep={depthStep}
16 {...subItem}
17 />
18 ))}
19 </List>
20 ) : null}
21 </>
22 )
23}
24
25function Sidebar({ items, depthStep, depth }) {
26 return (
27 <div className="sidebar">
28 <List disablePadding dense>
29 {items.map((sidebarItem, index) => (
30 <SidebarItem
31 key={`${sidebarItem.name}${index}`}
32 depthStep={depthStep}
33 depth={depth}
34 {...sidebarItem}
35 />
36 ))}
37 </List>
38 </div>
39 )
40}
در کد فوق برخی prop-های قدرتمند اعلان کردهایم تا فاز پیش رندر سایدبار مانند depth و depthStep را پیکربندی کنیم. SidebarItem به صورت کامپوننت خودش استخراج میشود و درون بلوک رندرش از depth برای محاسبه فاصلهبندی استفاده میکند. هر چه depth بالاتر باشد، در موقعیت عمیقتری در درخت قرار گرفته است. همه این موارد به دلیل وجود کد زیر ممکن شدهاند:
1{
2 items.map((subItem) => (
3 <SidebarItem
4 key={subItem.name}
5 depth={depth + 1}
6 depthStep={depthStep}
7 {...subItem}
8 />
9 ))
10}
depth هر بار که لیست جدید به آیتمهای فرعی اضافه شده و عمق بیشتری مییابد 1 واحد افزایش پیدا میکند. درون SidebarItem فرایند بازگشتی وجود دارد، زیرا خودش را تا زمانی که دیگر کد مبنا وجود نداشته باشد فراخوانی میکند. به بیان دیگر زمانی که آرایه خالی شد، این قطعه کد به صورت خودکار متوقف میشود:
1{
2 items.map((subItem) => (
3 <SidebarItem
4 key={subItem.name}
5 depth={depth + 1}
6 depthStep={depthStep}
7 {...subItem}
8 />
9 ))
10}
اینک کامپوننت سایدبار بازگشتی را تست میکنیم.
فایل src/App.js
1const items = [
2 { name: 'home', label: 'Home' },
3 {
4 name: 'billing',
5 label: 'Billing',
6 items: [
7 { name: 'statements', label: 'Statements' },
8 { name: 'reports', label: 'Reports' },
9 ],
10 },
11 {
12 name: 'settings',
13 label: 'Settings',
14 items: [
15 { name: 'profile', label: 'Profile' },
16 { name: 'insurance', label: 'Insurance' },
17 {
18 name: 'notifications',
19 label: 'Notifications',
20 items: [
21 { name: 'email', label: 'Email' },
22 {
23 name: 'desktop',
24 label: 'Desktop',
25 items: [
26 { name: 'schedule', label: 'Schedule' },
27 { name: 'frequency', label: 'Frequency' },
28 ],
29 },
30 { name: 'sms', label: 'SMS' },
31 ],
32 },
33 ],
34 },
35]
36
37function App() {
38 return (
39 <div>
40 <Sidebar items={items} />
41 </div>
42 )
43}
چنان که میبینید بالاخره موفق شدیم. اینک اندکی دیگر روی depthStep کار کرده و مقدار بالاتری را ارسال میکنیم:
1function App() {
2 return (
3 <div>
4 <Sidebar items={items} />
5 </div>
6 )
7}
سخن پایانی
سورس کد این راهنما را میتوانید از این ریپوی گیتهاب (+) دانلود کرده و قابلیتهای اضافی سایدبار را ببینید. این سایدبار کارکردهای جالبتری مانند افزودن یک لایه اضافی در رندرینگ دارد که منجر به نمایش جداکنندهها، باز کردن/ بستن سایدبار، آیکونها و غیره شده است.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- آموزش ساخت پروژه با فریم ورک React Native
- مجموعه آموزشهای برنامهنویسی
- 5 ابزار برای تسریع فرایند توسعه در React — راهنمای کاربردی
- آموزش ری اکت (React) — مجموعه مقالات مجله فرادرس
==