ساخت صفحه انتخاب کاراکتر در React (بخش دوم) — از صفر تا صد
در بخش قبلی این آموزش با مقدمات طراحی یک صفحه انتخاب کاراکتر در React آشنا شدیم. و اینک بخش دوم تقدیم شما میشود.
تابع onMorph کنونی حالت morphed را در زمان کلیک شدن به true تنظیم میکند و از این رو میتوانیم کلاس کاراکتر مخفی را که بازیکن انتخاب کرده است به محض true شدن morphed نمایش دهیم. ما آن را درست زیر عنصر div شامل دکمه morph نمایش میدهیم.
فایل src/App.js
1// at the top:
2import sageImg from './resources/sage.jpg'
3// ...
4
5{
6 morphed && (
7 <div className={styles.morphed}>
8 <Header>Congratulations!</Header>
9 <Content>
10 <div className={styles.characterBox}>
11 <img src={sageImg} />
12 </div>
13 </div>
14 <Subheader>
15 You have morphed into a <em>Sage</em>
16 </Subheader>
17 </div>
18 )
19}
فایل src/styles.module.css
1.morphed {
2 animation: flashfade 4s forwards;
3 opacity: 0;
4}
5@keyframes flashfade {
6 0% {
7 opacity: 0;
8 }
9 60% {
10 opacity: 0.7;
11 }
12 100% {
13 opacity: 1;
14 }
15}
کلاس مخفی که بازیکن به آن تبدیل میشود، یک Sage است که به صورت زیر نمایش مییابد:
گرچه این وضعیت میتواند مناسب تلقی شود، اما به نظر میرسد همچنان میتوان بهبودهایی روی آن اعمال کرد. به بیان دیگر به نظر میرسد که بازیکن به مقداری عمل اسکرول کردن نیاز دارد و از این رو باید با رابط کاربری سر و کله بزند.
در این مرحله یک کتابخانه کوچک به نام react-scroll-to-component نصب میکنیم که به ما امکان اسکرول کردن به صفحه بازیکن و اساساً به هر عنصر را با ارسال ارجاع آن عنصر میدهد:
npm install --save react-scroll-to-component
آن را در فایل src/App.js ایمپورت میکنیم:
import scrollToComponent from 'react-scroll-to-component'
اکنون یک ref ایجاد کرده و آن را به عنصر الصاق میکنیم:
1const App = () => {
2 const morphedRef = React.createRef()
3 const { selected, onSelect, morphed, onMorph } = useLevelUpScreen({ morphedRef })
4
5// ...
6
7 {morphed && (
8 <div
9 className={cx({
10 [styles.morphed]: morphed,
11 [styles.hidden]: !morphed,
12 })}
13 >
14 <Header>Congratulations!</Header>
15 <Content>
16 <div ref={morphedRef} className={styles.characterBox}>
17 <img src={sageImg} />
18 </div>
19 </Content>
20 <Subheader>
21 You have morphed into a <em>Sage</em>
22 </Subheader>
23 </div>
24 )}
از آنجا که میخواهیم جلوه اسکرول کردن، روان به نظر برسد، باید مقداری به ارتفاع صفحه اضافه کنیم تا فضای بیشتری پدید آید. این کار در عمل با افزودن یک div خالی با مقداری ارتفاع در زمان true شدن morphed ممکن خواهد بود:
1{
2 morphed && (
3 <div
4 className={cx({
5 [styles.morphed]: morphed,
6 [styles.hidden]: !morphed,
7 })}
8 >
9 <Header>Congratulations!</Header>
10 <Content>
11 <div ref={morphedRef} className={styles.characterBox}>
12 <img src={sageImg} />
13 </div>
14 </Content>
15 <Subheader>
16 You have morphed into a <em>Sage</em>
17 </Subheader>
18 </div>
19 )
20}
21{
22 morphed && <div style={{ height: '30vh' }} />
23}
اما اینک یک مشکل داریم. این ارتفاع اضافی در زمانی که ناحیه sega هنوز نمایان نشده است، ایجاد نمیشود. به بیان دیگر کارکرد اسکرول شدن کار نمیکند، زیرا در زمان فراخوانی هنوز فضای اضافی وجود ندارد. به منظور رفع این اشکال باید یک حالت دیگر به نام morphing اضافه کنیم که مقداری زمان به ما بدهد تا بتوانیم true شدن morphed بازیکن را در رابط کاربری معطل بکنیم:
1const useLevelUpScreen = ({ morphedRef }) => {
2 const [selected, setSelected] = React.useState([])
3 const [morphing, setMorphing] = React.useState(false)
4 const [morphed, setMorphed] = React.useState(false)
5
6 const onSelect = (type) => (e) => {
7 setSelected((prevSelected) => {
8 if (prevSelected.includes(type)) {
9 return prevSelected.filter((t) => t !== type)
10 }
11 return [...prevSelected, type]
12 })
13 }
14
15 const onMorph = () => {
16 setMorphing(true)
17 setTimeout(() => {
18 setMorphed(true)
19 setMorphing(false)
20 }, 1500)
21 }
22
23 React.useEffect(() => {
24 if (morphed) {
25 scrollToComponent(morphedRef.current, {
26 offset: 100,
27 align: 'middle',
28 duration: 1000,
29 })
30 }
31 }, [morphed, morphedRef])
32
33 return {
34 selected,
35 onSelect,
36 morphed,
37 onMorph,
38 morphing,
39 }
40}
اما اکنون با مشکل جدیدی مواجه شدهایم. به نظرمی رسد که morphed عناصر درون خود را از رندر شدن بازمیدارد و این مسدود شدن از بهکارگیری منطق درون قاب زمانی 1.5 ثانیهای ناشی میشود:
1const App = () => {
2 const morphedRef = React.createRef()
3 const { selected, onSelect, morphing, morphed, onMorph } = useLevelUpScreen()
4
5// ...
6
7{morphed && (
8 <div
9 className={cx({
10 [styles.morphed]: morphed,
11 [styles.hidden]: !morphed,
12 })}
13 >
14 <Header>Congratulations!</Header>
15 <Content>
16 <div ref={morphedRef} className={styles.characterBox}>
17 <img src={sageImg} />
18 </div>
19 </Content>
20 <Subheader>
21 You have morphed into a <em>Sage</em>
22 </Subheader>
23 </div>
24)}
25{morphed && <div style={{ height: '30vh' }} />}
برای حل این مشکل باید گزاره شرطی && morphed را برداریم و به جای آن از پکیج classnames برای ادغام استایلهای اضافی استفاده کنیم. این استایلها آن رفتار را شبیهسازی میکنند و عناصر را در درخت react حفظ میکنند. بدین ترتیب میتوانیم از قابلیتهایی مانند انیمیشن نیز استفاده کنیم:
1<div
2 className={cx({
3 [styles.morphed]: morphed,
4 [styles.hidden]: !morphed,
5 })}
6>
7 <Header>Congratulations!</Header>
8 <Content>
9 <div ref={morphedRef} className={styles.characterBox}>
10 <img src={sageImg} />
11 </div>
12 </Content>
13 <Subheader>
14 You have morphed into a <em>Sage</em>
15 </Subheader>
16</div>
17{
18 morphing || (morphed && <div style={{ height: '30vh' }} />)
19}
همچنین باید ref دیگر را نیز روی دکمه morph اعمال کنیم تا زمانی که کاربر یک کلاس کاراکتر را انتخاب میکند نیز صفحه اسکرول شود.
فایل src/App.js
1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2
3// ...
4
5const onSelect = (type) => (e) => {
6 setSelected((prevSelected) => {
7 if (prevSelected.includes(type)) {
8 return prevSelected.filter((t) => t !== type)
9 }
10 return [...prevSelected, type]
11 })
12 scrollToComponent(morphRef.current, {
13 offset: 300,
14 align: 'bottom',
15 duration: 1000,
16 })
17}
18
19const onMorph = () => {
20 if (!morphing) setMorphing(true)
21 setTimeout(() => {
22 setMorphing(false)
23 setMorphed(true)
24 }, 1500)
25}
26
27// ...
28
29return {
30 selected,
31 onSelect,
32 morphed,
33 morphing,
34 onMorph,
35}
36
37const App = () => {
38 const morphRef = React.createRef()
39 const morphedRef = React.createRef()
40 // ...
41 <div
42 ref={morphRef}
43 className={cx(styles.morph, {
44 [styles.hidden]: !selected.length,
45 })}
46 >
47 <MdKeyboardTab className={styles.morphArrow} />
48 <button
49 ref={morphRef}
50 name='morph'
51 type='button'
52 className={styles.morph}
53 style={{ opacity: morphed ? '0.4' : 1 }}
54 onClick={onMorph}
55 disabled={morphed}
56 >
57 {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
58 </button>
59 <MdKeyboardTab className={styles.morphArrowFlipped} />
60</div>
در مثال فوق زمانی که morph انجام شد، استایل زیر را اعمال کردیم:
style={{ opacity: morphed? '0.4': 1 }}
تا به بازیکن نشان دهیم که دکمه دیگر در دسترس نیست. یک خصوصیت غیر فعالسازی نیز برای غیر فعال کردن رویدادهای کلیک به صورت زیر اعمال کردیم:
disabled={morphed{
همچنین متن را بر اساس بهروزرسانی حالت morph با کد زیر تغییر دادیم:
{morphing? 'Morphing...': morphed? 'Morphed': 'Morph'}
تا کاربر به طور مداوم با نگاه کردن به چیزهایی که در حال تغییر هستند، مشغول باشد. همچنین بخش ) && selected.length !!} را حذف کردیم چون انیمیشن را مسدود میکرد و یک ref به نام morphRef چنان که در بخش قبلی دیدیم روی آن اعمال کردیم.
در نهایت در قلاب سفارشی، پیادهسازی scrollToComponent را نیز در انتهای تابع onSelect اعمال کردیم تا اسکرول را روی دکمه morph نیز انیمیت کنیم.
ویرایش نهایی
زمانی که morphing کامل شد نوعی انیمیشن بارگذاری را شبیهسازی میکنیم تا کاربر بداند که میخواهیم به بخش بعدی برویم:
1<div
2 className={cx(styles.next, {
3 [styles.hidden]: !ready,
4 })}
5>
6 <div>
7 <RingLoader size={60} color="rgb(213, 202, 255)" loading />
8 <p>Loading...</p>
9 </div>
10</div>
استایلهای آن چنین است:
1.next {
2 text-align: center;
3 margin: 35px auto;
4 display: flex;
5 justify-content: center;
6}
7.next p {
8 font-family: Patua One, sans-serif;
9 font-weight: 300;
10 text-align: center;
11 color: #fff;
12}
میتوانید ببینید که یک حالت جدید به نام ready نیز وجود دارد، بنابراین باید آن را در قلاب سفارشی پیادهسازی کنیم:
1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2 const [selected, setSelected] = React.useState([])
3 const [morphing, setMorphing] = React.useState(false)
4 const [morphed, setMorphed] = React.useState(false)
5 const [ready, setReady] = React.useState(false)
6
7 const onSelect = (type) => (e) => {
8 setSelected((prevSelected) => {
9 if (prevSelected.includes(type)) {
10 return prevSelected.filter((t) => t !== type)
11 }
12 return [...prevSelected, type]
13 })
14 scrollToComponent(morphRef.current, {
15 offset: 300,
16 align: 'bottom',
17 duration: 1000,
18 })
19 }
20
21 const onMorph = () => {
22 setMorphing(true)
23 setTimeout(() => {
24 setMorphing(false)
25 setMorphed(true)
26 }, 1500)
27 }
28
29 React.useEffect(() => {
30 if (morphed && !ready) {
31 scrollToComponent(morphedRef.current, {
32 offset: 100,
33 align: 'middle',
34 duration: 1000,
35 })
36 setTimeout(() => {
37 setReady(true)
38 }, 2000)
39 }
40 }, [morphed, morphedRef, ready])
41
42 return {
43 selected,
44 onSelect,
45 morphed,
46 morphing,
47 onMorph,
48 ready,
49 }
50}
در نهایت قصد داریم کل صفحه را فید کنیم تا با پایان یافتن بخش کنونی صفحههای بعدی را نمایش دهیم. این بدان معنی است که باید حالت دیگری به نام shutdown به قلاب سفارشی اضافه کنیم که فراخوانی میشود و یک نام کلاس جدید به عنصر div مربوط به root اعمال میکند. حالت shutdown تنها زمانی که ready به صورت true درآید مورد استفاده قرار میگیرد.
1const useLevelUpScreen = ({ morphRef, morphedRef }) => {
2 const [selected, setSelected] = React.useState([])
3 const [morphing, setMorphing] = React.useState(false)
4 const [morphed, setMorphed] = React.useState(false)
5 const [ready, setReady] = React.useState(false)
6 const [shutdown, setShutdown] = React.useState(false)
7
8 const onSelect = (type) => (e) => {
9 setSelected((prevSelected) => {
10 if (prevSelected.includes(type)) {
11 return prevSelected.filter((t) => t !== type)
12 }
13 return [...prevSelected, type]
14 })
15 scrollToComponent(morphRef.current, {
16 offset: 300,
17 align: 'bottom',
18 duration: 1000,
19 })
20 }
21
22 const onMorph = () => {
23 setMorphing(true)
24 setTimeout(() => {
25 setMorphing(false)
26 setMorphed(true)
27 }, 1500)
28 }
29
30 React.useEffect(() => {
31 if (morphed && !ready) {
32 scrollToComponent(morphedRef.current, {
33 offset: 100,
34 align: 'middle',
35 duration: 1000,
36 })
37 setTimeout(() => {
38 setReady(true)
39 }, 2000)
40 }
41 }, [morphed, morphedRef, ready])
42
43 React.useEffect(() => {
44 if (ready && !shutdown) {
45 setTimeout(() => {
46 setShutdown(true)
47 }, 2000)
48 }
49 }, [ready, shutdown])
50
51 return {
52 selected,
53 onSelect,
54 morphed,
55 morphing,
56 onMorph,
57 ready,
58 shutdown,
59 }
60}
61
62const App = () => {
63 const morphRef = React.createRef()
64 const morphedRef = React.createRef()
65
66 const {
67 selected,
68 onSelect,
69 morphing,
70 morphed,
71 onMorph,
72 ready,
73 shutdown,
74 } = useLevelUpScreen({
75 morphRef,
76 morphedRef,
77 })
78
79 const onClick = (e) => {
80 console.log("Don't mind me. I'm useless until I become useful")
81 }
82
83 return (
84 <div
85 className={cx(styles.root, {
86 [styles.shutdown]: shutdown,
87 })}
نتیجه نهایی چنین است:
اینک کد کامل به صورت زیر باید باشد.
فایل src/App.js
1import React from 'react'
2import cx from 'classnames'
3import { RingLoader } from 'react-spinners'
4import { MdKeyboardTab } from 'react-icons/md'
5import scrollToComponent from 'react-scroll-to-component'
6import noviceImg from './resources/novice.jpg'
7import sorceressImg from './resources/sorceress.jpg'
8import knightImg from './resources/knight.jpg'
9import sageImg from './resources/sage.jpg'
10import styles from './styles.module.css'
11import { Header, Subheader, Content } from './components'
12
13const useLevelUpScreen = ({ morphRef, morphedRef }) => {
14 const [selected, setSelected] = React.useState([])
15 const [morphing, setMorphing] = React.useState(false)
16 const [morphed, setMorphed] = React.useState(false)
17 const [ready, setReady] = React.useState(false)
18 const [shutdown, setShutdown] = React.useState(false)
19
20 const onSelect = (type) => (e) => {
21 setSelected((prevSelected) => {
22 if (prevSelected.includes(type)) {
23 return prevSelected.filter((t) => t !== type)
24 }
25 return [...prevSelected, type]
26 })
27 scrollToComponent(morphRef.current, {
28 offset: 300,
29 align: 'bottom',
30 duration: 1000,
31 })
32 }
33
34 const onMorph = () => {
35 setMorphing(true)
36 setTimeout(() => {
37 setMorphing(false)
38 setMorphed(true)
39 }, 1500)
40 }
41
42 React.useEffect(() => {
43 if (morphed && !ready) {
44 scrollToComponent(morphedRef.current, {
45 offset: 100,
46 align: 'middle',
47 duration: 1000,
48 })
49 setTimeout(() => {
50 setReady(true)
51 }, 2000)
52 }
53 }, [morphed, morphedRef, ready])
54
55 React.useEffect(() => {
56 if (ready && !shutdown) {
57 setTimeout(() => {
58 setShutdown(true)
59 }, 2000)
60 }
61 }, [ready, shutdown])
62
63 return {
64 selected,
65 onSelect,
66 morphed,
67 morphing,
68 onMorph,
69 ready,
70 shutdown,
71 }
72}
73
74const App = () => {
75 const morphRef = React.createRef()
76 const morphedRef = React.createRef()
77
78 const {
79 selected,
80 onSelect,
81 morphing,
82 morphed,
83 onMorph,
84 ready,
85 shutdown,
86 } = useLevelUpScreen({
87 morphRef,
88 morphedRef,
89 })
90
91 return (
92 <div
93 className={cx(styles.root, {
94 [styles.shutdown]: shutdown,
95 })}
96 >
97 <Header>
98 You are a <em>Novice</em>
99 </Header>
100 <Content>
101 <div
102 className={styles.characterBox}
103 style={{ width: 200, height: 150 }}
104 >
105 <img alt="" src={noviceImg} />
106 </div>
107 </Content>
108 <Subheader>Congratulations on reaching level 10!</Subheader>
109 <div style={{ margin: '25px auto' }}>
110 <Header>Choose your destiny</Header>
111 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
112 <Content>
113 <div
114 onClick={onSelect('Sorceress')}
115 className={cx(styles.characterBox, {
116 [styles.selectedBox]: selected.includes('Sorceress'),
117 })}
118 >
119 <h2>Sorceress</h2>
120 <img
121 alt=""
122 src={sorceressImg}
123 className={cx(styles.tier2, {
124 [styles.selected]: selected.includes('Sorceress'),
125 })}
126 />
127 </div>
128 <div
129 onClick={onSelect('Knight')}
130 className={cx(styles.characterBox, {
131 [styles.selectedBox]: selected.includes('Knight'),
132 })}
133 >
134 <h2>Knight</h2>
135 <img
136 alt=""
137 src={knightImg}
138 className={cx(styles.tier2, {
139 [styles.selected]: selected.includes('Knight'),
140 })}
141 />
142 </div>
143 </Content>
144 </div>
145 <div
146 ref={morphRef}
147 className={cx(styles.morph, {
148 [styles.hidden]: !selected.length,
149 })}
150 >
151 <MdKeyboardTab className={styles.morphArrow} />
152 <button
153 ref={morphRef}
154 name="morph"
155 type="button"
156 className={styles.morph}
157 style={{ opacity: morphed ? '0.4' : 1 }}
158 onClick={onMorph}
159 disabled={morphed}
160 >
161 {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
162 </button>
163 <MdKeyboardTab className={styles.morphArrowFlipped} />
164 </div>
165 <div
166 className={cx({
167 [styles.morphed]: morphed,
168 [styles.hidden]: !morphed,
169 })}
170 >
171 <Header>Congratulations!</Header>
172 <Content>
173 <div ref={morphedRef} className={styles.characterBox}>
174 <img src={sageImg} />
175 </div>
176 </Content>
177 <Subheader>
178 You have morphed into a <em>Sage</em>
179 </Subheader>
180 </div>
181 <div
182 className={cx(styles.next, {
183 [styles.hidden]: !ready,
184 })}
185 >
186 <div>
187 <RingLoader size={60} color="rgb(213, 202, 255)" loading />
188 <p>Loading...</p>
189 </div>
190 </div>
191 </div>
192 )
193}
194
195export default App
فایل src/components.js
1import React from 'react'
2import cx from 'classnames'
3import styles from './styles.module.css'
4
5export const Header = ({ children, ...rest }) => (
6 // eslint-disable-next-line
7 <h1 className={styles.header} {...rest}>
8 {children}
9 </h1>
10)
11
12export const Subheader = ({ children, ...rest }) => (
13 <small className={styles.subheader} {...rest}>
14 {children}
15 </small>
16)
17
18export const Content = ({ children, ...rest }) => (
19 <div className={styles.container} {...rest}>
20 {children}
21 </div>
22)
فایل src/styles.module.css
1body {
2 margin: 0;
3 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
4 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
5 'Helvetica Neue', sans-serif;
6 -webkit-font-smoothing: antialiased;
7 -moz-osx-font-smoothing: grayscale;
8 background: rgb(23, 30, 34);
9}
10
11code {
12 font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 monospace;
14}
15
16.root {
17 padding: 20px 0;
18}
19
20.container {
21 display: flex;
22 justify-content: center;
23}
24
25.header {
26 text-align: center;
27 color: rgb(252, 216, 169);
28 font-weight: 300;
29 margin: 0;
30}
31
32.subheader {
33 color: #fff;
34 text-align: center;
35 font-weight: 300;
36 width: 100%;
37 display: block;
38}
39
40.characterBox {
41 transition: all 0.1s ease-out;
42 width: 300px;
43 height: 250px;
44 border: 1px solid rgb(194, 5, 115);
45 background: rgb(82, 26, 134);
46 margin: 12px 6px;
47 overflow: hidden;
48}
49
50.characterBox img {
51 width: 100%;
52 height: 100%;
53 object-fit: cover;
54 cursor: pointer;
55}
56
57.selectedBox {
58 border: 1px solid rgb(24, 240, 255) !important;
59}
60
61.characterBox h2 {
62 transition: all 0.3s ease-out;
63 text-align: center;
64 color: rgb(213, 202, 255);
65 font-style: italic;
66 font-weight: 500;
67}
68
69.characterBox:hover h2 {
70 color: rgb(191, 255, 241);
71}
72
73.characterBox img {
74 transition: all 0.3s ease-out;
75 width: 100%;
76 height: 100%;
77 object-fit: cover;
78 cursor: pointer;
79}
80
81.characterBox img.tier2:hover,
82.characterBox img.selected {
83 animation: hueRotate 2s infinite;
84 transform: scale(1.05);
85}
86
87.morph {
88 margin: 30px auto;
89 text-align: center;
90}
91
92.morphArrow {
93 color: rgb(123, 247, 199);
94 transform: scale(2);
95 animation: morphArrow 2s infinite;
96}
97
98.morphArrowFlipped {
99 composes: morphArrow;
100 transform: scale(-2, 2);
101}
102
103@keyframes morphArrow {
104 0% {
105 opacity: 1;
106 color: rgb(123, 247, 199);
107 }
108 40% {
109 opacity: 0.4;
110 color: rgb(248, 244, 20);
111 }
112 100% {
113 opacity: 1;
114 color: rgb(123, 247, 199);
115 }
116}
117
118button.morph {
119 cursor: pointer;
120 transition: all 0.2s ease-out;
121 border-radius: 25px;
122 padding: 14px 22px;
123 color: #fff;
124 background: rgb(35, 153, 147);
125 border: 1px solid #fff;
126 font-family: Patua One, sans-serif;
127 font-size: 1.2rem;
128 text-transform: uppercase;
129 letter-spacing: 2px;
130 margin: 0 20px;
131}
132
133button.morph:hover {
134 background: none;
135 border: 1px solid rgb(35, 153, 147);
136 color: rgb(35, 153, 147);
137}
138
139.morphed {
140 animation: flashfade 4s forwards;
141 opacity: 0;
142}
143
144@keyframes flashfade {
145 0% {
146 opacity: 0;
147 }
148 60% {
149 opacity: 0.7;
150 }
151 100% {
152 opacity: 1;
153 }
154}
155
156.hidden {
157 visibility: hidden;
158}
159
160.next {
161 text-align: center;
162 margin: 35px auto;
163 display: flex;
164 justify-content: center;
165}
166
167.next p {
168 font-family: Patua One, sans-serif;
169 font-weight: 300;
170 text-align: center;
171 color: #fff;
172}
173
174@keyframes hueRotate {
175 0% {
176 filter: hue-rotate(0deg);
177 }
178 50% {
179 filter: hue-rotate(260deg) grayscale(100%);
180 }
181 100% {
182 filter: hue-rotate(0deg);
183 }
184}
185
186.shutdown {
187 animation: shutdown 3s forwards;
188}
189
190@keyframes shutdown {
191 100% {
192 opacity: 0;
193 }
194}
ممکن است متوجه شده باشید که در سراسر این راهنما برخی کدهای تکراری وجود داشتند. فرض کنید مجبور باشید یک تغییر ناگهانی روی کادرهای انتخاب کاراکتر داشته باشید، مثلاً اندازه آنها را تغییر دهید. بدین ترتیب اگر یکی از آنها را عوض کنید، باید کل فایل را اسکن کنید و کادر انتخاب دیگر را پیدا کنید تا آن را نیز تغییر دهید تا رابط کاربری یک دست بماند.
در حال حاضر، کادرهای انتخاب Sorceress و Knight یکسان هستند و باید به صورت یکسانی نیز بمانند. اما اگر دو کاراکتر tier2 دیگر به بازی اضافه کنیم چطور؟ بدین ترتیب باید کلی کد تکراری داشته باشیم. از این رو ایده بهتر این است که آن را به صورت یک کامپوننت مستقل تجرید کنیم. این کار مزیت عمدهای به صورت امکان سفارشیسازی بهتر نیز فراهم میآورد.
در صورتی که کادرهای انتخاب کاراکتر را تجرید کنیم، کد به صورت زیر در میآید:
فایل src/App.js
1const characterSelections = [
2 { type: 'Sorceress', src: sorceressImg },
3 { type: 'Knight', src: knightImg },
4 { type: 'Shapeshifter', src: shapeshifterImg },
5 { type: 'Bandit', src: banditImg },
6 { type: 'Archer', src: archerImg },
7 { type: 'Blade Master', src: bladeMasterImg },
8 { type: 'Destroyer', src: destroyerImg },
9 { type: 'Summoner', src: summonerImg },
10 { type: 'Phantom', src: phantomImg },
11]
12
13const charSelectionMapper = characterSelections.reduce(
14 (acc, { type, src }) => ({
15 ...acc,
16 [type]: src,
17 }),
18 {},
19)
20
21const App = () => {
22 const morphRef = React.createRef()
23 const morphedRef = React.createRef()
24
25 const {
26 selected,
27 onSelect,
28 morphing,
29 morphed,
30 onMorph,
31 ready,
32 shutdown,
33 } = useLevelUpScreen({
34 morphRef,
35 morphedRef,
36 })
37
38 return (
39 <div
40 className={cx(styles.root, {
41 [styles.shutdown]: shutdown,
42 })}
43 >
44 <Header>
45 You are a <em>Novice</em>
46 </Header>
47 <Content>
48 <CharacterBox
49 style={{ width: 200, height: 150 }}
50 imgProps={{ src: noviceImg }}
51 disableFlashing
52 />
53 </Content>
54 <Subheader>Congratulations on reaching level 10!</Subheader>
55 <div style={{ margin: '25px auto' }}>
56 <Header>Choose your destiny</Header>
57 <Subheader>Choose one. Or all, if you know what I mean.</Subheader>
58 <Content display="grid">
59 {characterSelections.map((props, index) => (
60 <CharacterBox
61 key={`char_selection_${index}`}
62 onClick={onSelect(props.type)}
63 isSelected={selected === props.type}
64 {...props}
65 />
66 ))}
67 </Content>
68 </div>
69 <div
70 ref={morphRef}
71 className={cx(styles.morph, {
72 [styles.hidden]: !selected,
73 })}
74 >
75 <MdKeyboardTab className={styles.morphArrow} />
76 <button
77 ref={morphRef}
78 name="morph"
79 type="button"
80 className={styles.morph}
81 style={{ opacity: morphed ? '0.4' : 1 }}
82 onClick={onMorph}
83 disabled={morphed}
84 >
85 {morphing ? 'Morphing...' : morphed ? 'Morphed' : 'Morph'}
86 </button>
87 <MdKeyboardTab className={styles.morphArrowFlipped} />
88 </div>
89 <div
90 className={cx({
91 [styles.morphed]: morphed,
92 [styles.hidden]: !morphed,
93 })}
94 >
95 <Header>Congratulations!</Header>
96 <Content>
97 <CharacterBox
98 ref={morphedRef}
99 type={selected}
100 headerProps={{ className: styles.unique }}
101 imgProps={{ src: charSelectionMapper[selected] }}
102 />
103 </Content>
104 <Subheader>
105 You have morphed into a <em>{selected}</em>
106 </Subheader>
107 </div>
108 <div
109 className={cx(styles.next, {
110 [styles.hidden]: !ready,
111 })}
112 >
113 <div>
114 <RingLoader size={60} color="rgb(213, 202, 255)" loading />
115 <p>Loading...</p>
116 </div>
117 </div>
118 </div>
119 )
120}
فایل src/components.js
1// ...
2
3const CharacterBox = React.forwardRef(
4 (
5 {
6 isSelected,
7 type,
8 headerProps = {},
9 imgProps = {},
10 src,
11 disableFlashing,
12 ...rest
13 },
14 ref,
15 ) => (
16 <div
17 ref={ref}
18 className={cx(styles.characterBox, {
19 [styles.selectedBox]: isSelected,
20 })}
21 {...rest}
22 >
23 {type && <h3 {...headerProps}>{type}</h3>}
24 <img
25 {...imgProps}
26 src={src || imgProps.src}
27 className={cx(styles.tier2, imgProps.className, {
28 [styles.selected]: isSelected,
29 [styles.noAnimation]: !!disableFlashing,
30 })}
31 alt=""
32 />
33 </div>
34 ),
35)
سخن پایانی
بدین ترتیب به انتهای این مقالهی دو بخشی میرسیم. امیدواریم از این راهنما بهره لازم را برده باشید و توانسته باشیم به دانش شما در این زمینه بیافزاییم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- واکشی (Fetch) کردن داده ها در اپلیکیشن های React — به زبان ساده
- ۲۲ ابزار مهم برای توسعه دهندگان React — فهرست کاربردی
- آموزش ری اکت (React) — مجموعه مقالات مجله فرادرس
==