آموزش: مقدمهای بر ریاکت
فرض این آموزش بر این است که شما هیچ دانش قبلی از ریاکت ندارید.
قبل از اینکه آموزش را شروع کنیم
در طول این آموزش، ما یک بازی کوچک خواهیم ساخت. ممکن است قصد داشته باشید که از خواندن آن صرفنظر کنید، چونکه شما قصد بازیسازی ندارید، اما به آن فرصتی دهید. تکنیکهای مورد استفاده در این آموزش، مبانی ساخت هر برنامه ریاکت است و تبحر در آن میتواند به شما درک عمیقی از ریاکت دهد.
نکته
این آموزش برای افرادی طراحی شده که ترجیح میدهند به صورت پروژهمحور و با حل مثالها، مباحث را بیاموزند. در صورتی که تمایل دارید مسائل را از پایه بیاموزید به راهنمای گامبهگام ریاکت رجوع کنید. شاید برای شما این آموزش و راهنمای گامبهگام مکمل همدیگر باشند.
این آموزش به چند بخش تقسیم میشود:
- آمادهسازی: آمادهسازی سیستم و نصب ابزارهای مورد نیاز
- بررسی اجمالی: یادگیری مفاهیم اصلی ریاکت مثل کامپوننتها، props و state
- کامل کردن بازی: یادگیری مهمترین تکنیکهای برنامهنویسی ریاکت
- اضافه کردن دکمهی برگشت: یک دید عمیقتر به تواناییهای منحصر به فرد ریاکت.
برای دریافت نکات و یادگیری بهتر، خوب است که به جای خواندن و تمام کردن همهی بخشهای این آموزش، آنها را مرور و تمرین و تکرار کنید هرچند که ممکن است اینکار باعث شود که مقدار بخشهای کمتری را در طول زمان معینی بخوانید ولی این کار را بسیار به شما توصیه میکنیم.
چه خواهیم ساخت؟
در این آموزش، با استفاده از ریاکت در نهایت یک بازی tic-tac-toe یا همان دوز با رابط تعاملی میسازیم.
میتوانید کد نهایی بازی را از نتیجهی نهایی بررسی کنید. اگر چیزی متوجه نشدید و نحوهی نگارش کدها برایتان ناآشنا بود نگران نباشید به زودی آشنا خواهید شد و بدانید که هدف این آموزش ایناست که درک خوبی از ریاکت و نحوهی نگارش آن به دست آورید.
شدیدا توصیه میشود قبل شروع این آموزش، به خود بازی نگاهی بیندازید. یکی از ویژگیهایی که باید به آن توجه کنید لیست عددهایی است که در سمت راست بازی نمایش داده میشوند. اینها یک تاریخچه از حرکتهایی بازی را نشان میدهند و در طول بازی تغییر میکند.
حالا اگر با این بازی آشنایی دارید میتوانید آن را ببندید. ما ابتدا از قالب سادهتری شروع میکنیم و در مرحلهی بعد شما را آماده میکنیم تا شروع به ساخت بازی کنید.
پبشنیازها
در این آموزش فرض شده که شما مهارتهایی در زمینهی HTML و جاوا اسکریپ دارید ولی در غیر این صورت اگر از یک زبان برنامهنویسی دیگری میآیید هم نباید برایتان مشکلی پیش بیایید. ما همچنین فرض میکنیم که شما با مفاهیم برنامهنویسی همچون توابع، شیءها، آرایهها، و کمی کلاسها آشنایی دارید.
اگر نیاز به کمی دوره کردن دستورات جاوااسکریپت دارید، پیشنهاد میکنیم این راهنما را مطالعه کنید. توجه داشته باشید که ما همچنین از برخی امکانات ES6 (نسخهی اخیر جاوااسکریپت) استفاده خواهیم کرد همچون توابع Arrow ، کلاسها و عبارات let و const. شما همچنین میتوانید از طریق Babel REPL چک کنید که کدهای ES6 به چه چیزهایی تبدیل میشوند.
آمادهسازی
دو راه برای کاملکردن این آموزش دارید: میتوانید کد را در مرورگر خود بنویسید یا یک محیط توسعه لوکال روی رایانه خود آماده کنید.
روش ۱: نوشتن و اجرای کدها در مرورگر
این سریعترین راه برای شروع به یادگیری است!
ابتدا کد اولیه را در تب جدید مرورگر باز کنید. یک بازی(البته ناقص) و در سمت دیگر کدهای ریاکت را خواهید دید. حالا شما میتوانید کدها رو تغییر داده و کدهای خودتان را اجرا کنید.
اکنون میتوانید روش دوم را رد کنید و به بخش نمای کلی رفته تا یک نگاهی کلی به ریاکت بیندازید.
روش ۲: راهاندازی محیط محلی یا لوکال ریاکت
کاملا اختیاری و برای این آموزش الزامی نیست.
اختیاری: دستورات برای دنبالکردن آموزش به صورت لوکال با استفاده از ویرایشگر متن مورد انتخابتون
این روش نیازمند صرف توان و زمان بیشتری برای راهاندازی است اما در عوض به شما این اجازه را میدهد که با ویرایشگر دلخواهتان کدها را ویرایش کنید. در اینجا این مراحل را باید دنبال کنید:
- ابتدا اطمینان حاصل کنید که یک نسخهی اخیر Node.js را بر روی سیستم نصب دارید.
- راهنمای ایجاد یک پروژهی ریاکت را دنبال کنید تا یک پروژه جدید بسازید.
npx create-react-app my-app
- تمامی فایلهای داخل پوشهی
src/
پروژهی ایجاد شده را پاک کنید.
نکته:
به جای پاک کردن کل پوشهی
src
فقط سورسکدهای داخل آن را پاک کنید. در مرحلهی بعد آنها را با فایلهای مورد نظرمان جایگزین خواهیم کرد.
cd my-app
cd src
# اگر از لینوکس یا مک استفاده میکنید:
rm -f *
# یا اگر برروی ویندوز هستید:
del *
# بعد به پوشهی پروژه بر میگردیم:
cd ..
- در پوشهی
src/
یک فایل با نام index.css
و با محتویات این کد CSS ایجاد کنید. - در پوشهی
src/
یک فایل دیگر به نامindex.js
با متحویات این کد جاوااسکریپت ایجاد کنید.
۶. این سه خط را به بالای index.js
اضافه کنید:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
حالا اگر npm start
را در پوشهی پروژه اجرا و در مرورگر http://localhost:3000
را باز کنید، باید یک بازی tic-tac-toe خالی ببینید.
برای تنظیم رنگبندی کدها (syntax highlighting) ویرایشگر خود، ما دنبالکردن این دستورالعملها را پیشنهاد میکنیم.
کمک، به مشکل برخوردم کردم!
اگر به مشکل برخوردید به منابع کمکی کامیونیتی رجوع کنید. به خصوص Reactiflux Chat میتواند روشی سریع برای کمک گرفتن باشد. اگر جوابی دریافت نکردید، یا پیشرفتی حاصل نشد، یک issue ثبت کنید و ما به شما کمک خواهیم کرد.
نمای کلی
حالا که آمادهاید بیایید یک دید کلی از ریاکت به دست آوریم.
ریاکت چیست؟
ریاکت یک کتابخانه کارآمد و انعطافپذیر برای جاوااسکریپت است که به شما این اجازه را میدهد که از تکههای کوچک و ساده رابط کاربریهای پیچیده بسازید. این تکههای کوچک کامپوننتها(components) نام دارند.
در ریاکت چند نوع کامپوننت وجود دارد که با زیر کلاس React.Component
شروع میکنیم:
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>لیست خرید برای {this.props.name}</h1>
<ul>
<li>اینستاگرام</li>
<li>واتساپ</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// برای استفاده از کامپوننت ساخته شده میتوان برای مثال آن را به این صورت تعریف کرد: <ShoppingList name="Mark" />
به زودی به بخشهای جالب تگهای XML مانند هم میرسیم. ما با استفاده از کامپوننتها به ریاکت توضیح میدهیم که میخواهیم چه چیزی باید روی صفحه نمایان شود. وقتی که دادههای ما دچار تغییر شود، ریاکت به صورت کارآمد کامپوننتهای ما را بهروزرسانی و دوباره رندر میکند.
در این جا Shopping List از کلاس کامپوننت ریاکت یا در اصل از نوعی کامپوننت ریاکت است. در ریاکت هر کامپوننت میتواند پارامترهایی را هنگام فراخوانی دریافت کند که به آنها props
(کوتاه شدهی “properties” به معنی “ویژگیها”) گفته میشود و سلسلهمراتبی از چشماندازهایی که باید بر روی صفحه نمایش داده شوند از طریق متد render
برگشت میدهد.
متد render
توضیحاتی مربوط به عناصری که میخواهیم بر روی صفحهنمایش ببینیم را بر میگرداند و سپس ریاکت توضیحات را دریافت کرده و نتیجه را بر روی صفحه نمایش میدهد. در اصل متد render
یک المنت ریاکت بر میگرداند که توضیح مختصری دربارهی آنچیزی که باید رندر شود، در بردارد. اکثر برنامهنویسان ریاکت همچون کد بالا از “JSX” برای مشخص کردن و قرار دادن عناصر استفاده میکنند که باعث میشود نوشتن ساختار تگها آسانتر شود. برای مثال در JSX به جای ساخت المنت با دستورReact.createElement("div")
از <div />
استفاده میشود. این کدهای شبه XML در زمان تحلیل به نوع اصلی و اشیاء جاوااسکریپت تبدیل میگردند. برای مثال در متد render
مثال بالا میتوان از این کد استفاده کرد:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
مشاهدهی کد کاملا گسترش یافته.
اگر کنجکاو هستید createElement()
در مرجع API با جزئیات بیشتری توضیح داده شده است ولی در این آموزش از آن استفاده نخواهیم کرد و با JSX ادامه خواهیم داد.
در JSX نیز شما میتوانید از تمام قدرت جاوااسکریپت بهرهمند شوید، تنها کافی است هر عبارت جاوااسکریپتی که میخواهید را درون آکولاد قرار دهید. هر المنت ریاکت دراصل یک شیء جاوااسکریپت است که میتواند در یک متغیر ذخیره شود یا به نقاط مختلف برنامه پاس دادهشوند.
در بالا کامپوننت ShoppingList
تنها چند کامپوننت توکار DOM مثل <div />
و <li />
را رندر میکند. اما شما میتوانید کاپوننتهای سفارشی ریاکت ساخته و رندر کنید. برای مثال ما میتوانیم با نوشتن <ShoppingList />
به کل لیست خرید ارجاع دهیم. در ریاکت همه کامپونتتها محصور شده و به صورت مستقل عمل میکنند. همین ویژگی به شما اجازه میدهد از کامپوننتهای ساده رابطهای گرافیکی پیچیده بسازید.
بررسی کد اولیه
اگر ریاکت را از طریق مرورگر اجرا میکنید، کد اولیه را در یک تب جدید باز کنید و در صورتی که ریاکت را به صورت محلی نصب کردهاید در پوشهی پروژهی خود src/index.js
را با ویرایشگر دلخواه خود باز کنید.(شما قبلا در طول راهاندازی محیط لوکال این فایل را ایجاد کرده بودید)
این کد اولیه پایهی اصلی بازی ماست و ما بازی را بر روی آن پیادهسازی میکنیم. ما قبلا کدهای CSS مورد نیاز را برای شما قرار دادهایم تا شما فقط روی یادگیری ریاکت و برنامهنویسی بازی tic-tac-toe تمرکز کنید.
با کمی نگاه و بررسی کدها متوجه خواهید شد که در آن سه کامپوننت ریاکت وجود دارد:
- Square
- Board
- Game
کامپوننت Square یک <button>
رندر میکند و هر کامپوننت Board، نُه Square را رندر میکند. کامپوننت Game هم یک Board به همراه مقادیر اولیه که بعدا آنها را تغییر میدهیم رندر میکند. فعلا در این کد هیچ کامپوننت تعاملی و فعالی وجود ندارد.
گذر دادن اطلاعات از طریق Props
وقت آن رسیده تا شروع به کامل کردن کد اولیه کنیم. کاری که در ابتدا انجام میدهیم آن است که مقداری داده را از کامپوننت Board به Square انتقال دهیم.
شدیدا به شما توصیه میکنیم که به جای کپی پیست کردن کدها، آنها را تایپ کنید. این ترفند به شما کمک میکند که درک بهتری از ریاکت و مفاهیم آن پیدا کرده و حافظه عضلانیتان را نیز تقویت کنید.
اکنون در متد renderSquare
کد را به صورت زیر تغییر میدهیم تا یک prop به نام value
را به Square بفرستیم:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />; }
}
حال برای نمایش مقدار value در مربعها {this.props.value}
را جایگزین {/* TODO /*}
میکنیم. در نهایت کلاس Square ما به این شکل خواهد بود:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value} </button>
);
}
}
قبل از اعمال تغییرات:
بعد اعمال: شما باید عددی را در هر مربع در خروجی رندرشده ببینید.
تبریک، شما یک prop را از کامپوننت Board والد به یک کامپوننت Square فرزند انتقال دادید. عبور دادنpropها نحوه جریان اطلاعات در برنامههای ریاکتی است. از والد به فرزند.
ساخت یک کامپوننت تعاملی
بیایید کاری کنیم که وقتی روی هر کامپوننت Square کلیک شد، روی آن “X” را نمایش داده شود. ابتدا تگ button را که از کامپوننت Square برگشت داده میشود را تغییر میدهیم:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { console.log('click'); }}> {this.props.value}
</button>
);
}
}
اگر اکنون بر روی یک مربع کلیک کنید، باید ‘click’ را در کنسول devtools مرورگر خود مشاهده کنید.
نکته
در اینجا و از این به بعد برای اینکه در تایپ کردن صرفهجویی کرده و سردگمی هنگام کار با
this
خلاص شویم در کنترل رخدادها و مانند کد پایین از توابع Arrow استفاده میکنیم:class Square extends React.Component { render() { return ( <button className="square" onClick={() => console.log('click')}> {this.props.value} </button> ); } }
دقت کنید با استفاده از
onClick={() => console.log('click')}
یک تابع را به عنوان proponClick
این دکمه قرار دادهایم پس ریاکت تنها بعد از هر کلیک این تابع را اجرا خواهد کرد. فراموشکردن() =>
و نوشتنonClick={console.log('click')}
یک اشتباه بسیار رایج است که باعث میشود هر زمانی که ریاکت کامپوننت را دوباره رندر میکند پیاممان نشان داده شود.
به عنوان مرحلهی بعدی، میخواهیم کامپوننت Square “به خاطر بسپارد” که کلیک میشود و علامت “X” نمایش دهد. برای اینکه کامپوننتی چیزی را به خاطر بسپارد از state استفاده میکنیم.
برای استفاده از state، ابتدا باید this.state
را در constructor آن تعریف کنیم. this.state
باید به عنوان خصوصی (private) برای کامپوننت ریاکتی که در آن تعریف شده است در نظر گرفته شود. حال بیایید مقدار فعلی Square را در this.state
ذخیره کرده و هر زمان که روی آنها کلیک شد آن را تغییر دهیم.
ابتدا متد constructor را به منظور مقدار دهی اولیه state ایجاد میکنیم:
class Square extends React.Component {
constructor(props) { super(props); this.state = { value: null, }; }
render() {
return (
<button className="square" onClick={() => console.log('click')}>
{this.props.value}
</button>
);
}
}
نکته
در کلاسهای جاوااسکریپت، باید همیشه در هنگام ساخت constructor یک کلاس که از یک کلاس دیگر منشعب میشود(ارث میبرد)،
super
را صدا بزنیم. پس همهی کامپوننتهای ریاکت که نیازمند به متدconstructor
هستند باید با صدازدنsuper(props)
شروع شوند.
اکنون متد render
Square را طوری تغییری میدهیم تا زمانی که کلیک شد مقدار فعلی state را نمایش دهد:
- در تگ
<button>
عبارتthis.props.value
را باthis.state.value
جایگزین کنید. - عبارت
onClick={...}
را باonClick={() => this.setState({value='X'})}
جایگزین کنید. - برای خوانایی بیش تر propهای
className
وonClick
را در خط های جداگانه قرار دهید.
بعد از این تغییرات باید تگ <button>
که از متد render
Square برگشت داده میشود چیزی شبیه به این باشد:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square" onClick={() => this.setState({value: 'X'})} >
{this.state.value} </button>
);
}
}
با صدا زدن this.setState
از کنترلر onClick
واقع در متد render
Square، به ریاکت میگوییم هر زمانی که <button>
کلیک شد، Square را دوباره رندر کند. بعد از بهروزرسانی مقدار this.state.value
Square برابر 'X'
خواهدشد تا X
بر روی صفحه بازی ظاهر میشود. اگر روی هر Square کلیک کنید ، X باید نشان داده شود.
وقتی که متد setState
را درون یک کامپوننت صدا میزنید، ریاکت به صورت اتوماتیک کامپوننتهای فرزند درون آن را نیز دوباره بهروزرسانی میکند.
ابزارهای برنامهنویسان
افزونهی React Devtools را برای کروم یا فایرفاکس نصب کنید. این افزونه به شما اجازه میدهد کامپوننتهای ریاکت را در قسمت developer tools مرورگر خود مشاهده و بازبینی کنید. [در اکثر مرورگرها با فشردن دکمهی F12 وارد قسمت برنامه نویسان خواهید شد.]
با استفاده از افزونه React DevTools میتوانید propها و stateهای هر کامپوننت را چک و مورد بررسی قرار دهید.
بعد از نصب افزونه با راستکلیککردن روی هر المنت درون صفحه و انتخاب گزینهی Inspect صفحهی developer tools باز شده و تبهای ریاکت (“⚛️ Components” و “⚛️ Profiler”) در آخرین تب در سمت راست ظاهر میشود. از “⚛️ Components” برای بررسی درخت کامپوننتها استفاده کنید.
اما برای این که این افزونه با CodePen هم کار کند نیازمند چند کار کوچک هستیم:
- ثبت نام یا ورود کرده و در صورت ثبت نام ایمیل خود را تایید کنید (این کار به منظور جلوگیری از هرزنامه یا اسپم است).
- روی دکمهی Fork کلیک کنید.
- روی Change View کلیک کرده و Debug mode را انتخاب کنید.
- در تب جدید باز شده، developer tools باید دارای تب مخصوص ریاکت باشد.
کاملکردن بازی
ما در حال حاضر قطعات اصلی ساخت بازیمان را در اختیار داریم و چیزی که هماکنون نیاز داریم این است که روی صفحهی بازی به نوبت “X” و “O” قرار بگیرد و در مرحلهی بعد نیاز به راهی داریم تا بتوانیم برندهی بازی را تعیین کنیم.
بالا بردن state (انتقال state به کامپوننتهای بالاتر)
هم اکنون هر Square یک state مربوط به خودش را نگه میدارد. برای یافتن برنده ما نیازمندیم مقدارهای همه مربع ها را در یک جا داشته باشیم.
شاید تصور کنید Board باید از هر Square مقدار state را بپرسد و در مکانی ذخیره کند. با اینکه این حالت در ریاکت امکان پذیر است اما پیشنهاد میکنیم که از آن استفاده نکنید زیرا که باعث سخت شدن فهم کد، مستعد به افزایش باگها، سخت شدن دوبارهنویسی و بهبود کد میشود. در عوض ما میتوانیم که حالت و موقعیت بازی را به جای درون هر Square در کامپوننت والدشان یعنی Board ذخیره کنیم. بنابراین Board با فرستادن یک prop به هر Square به او میگوید که چه چیزی را باید نمایش دهد. شبیه به کاری که برای فرستادن یک عدد به Squareها انجام داده بودیم.
برای جمع کردن دادهها از چند کامپوننت فرزند یا داشتن دو کامپوننت فرزند که با هم تعامل و ارتباط داشته باشند نیاز به اعلام state به اشتراک گذاشتهشدهیشان در کامپوننت والدشان داریم. کامپوننت والد state را با استفاده از عبوردادن props به فرزندانش منتقل میکند. این کار کامپوننتهای فرزند را با یکدیگر و با کامپوننت والد هماهنگ می کند.
در اینگونه موارد انتقال state به کامپوننت والد (بالا بردن state) بسیار رایج است، پس بیایید از این فرصت برای امتحانش استفاده کنیم.
یک constructor به Board اضافه میکنیم و مقدار اولیه state برای Board را یک آرایه با ۹ عضو (مربوط به ۹ مربع بازی) که مقادیر همگی null است قرار دهید.
class Board extends React.Component {
constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; }
renderSquare(i) {
return <Square value={i} />;
}
در طول بازی، آرایه this.state.squares
چیزی شبیه زیر میباشد:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
هم اکنون متد renderSquare
کامپوننت Board به این صورت است:
renderSquare(i) {
return <Square value={i} />;
}
در ابتدا، prop به نام value
از طرف Board به هر Square برای نمایش ۰ تا ۸ فرستاده میشد اما در مرحلهی قبل علاوه براین که “X” را جایگزین عدد کردیم مقدار هر Square را نیز بر اساس state خودش قرار دادیم. به همین دلیل است که Square هماکنون prop به نام value
که از طرف Board به آن ارسال میشود را نادیده میگیرد.
الان دوباره از مکانیزم انتقال prop استفاده خواهیم کرد. ابتدا باید در کلاس Board تغییراتی دهیم تا به هر Square مقدار مخصوصش ('X'
، 'O'
یا null
) را بفرستد. ما پیش از این آرایهی squares
را در متد constructor مربوط به Board تعریف کردهایم و الان تنها نیاز است تغییراتی را در متد renderSquare
کلاس Board ایجاد کنیم تا از آن بخواند:
renderSquare(i) {
return <Square value={this.state.squares[i]} />; }
هر Square یک prop به نام value
دریافت میکند که دارای یکی از مقادیر 'X'
، 'O'
، یا null
برای مربعهای خالی است.
در مرحلهی بعد نیاز داریم فرآیندی که هنگام کلیک یک مربع اجرا میشود را تغییر دهیم. هم اکنون Board، لیست مقدار مربعها را در خود نگه میدارد و کاری که باید انجام دهیم این است که راهی ایجاد کنیم تا در هنگام بازی Squareها بتوانند مقدار state بورد را به روز کنند. توجه کنید که stateها به صورت خصوصی هستند و از کامپوننتهای دیگر قابل دسترسی و تغییر نیستند.
اما به جای آن میتوانیم تابعی را(برای تغییر state مربعها) از Board به Square فرستاده تا Square هر وقت لازم داشت (در اینجا کلیک) آن را صدا بزند. پس متد renderSquare
را به شکل زیر تغییر میدهیم:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)} />
);
}
نکته
برای خوانایی بیشتر، المنتی که تابع برمیگرداند را در خطهای جداگانه مینویسیم و آنهای را درون پرانتز قرار میدهیم تا جاوااسکریپت یک سمیکالن را به بعد از
return
اضافه نکند و کد ما را از کار نیندازد.
حالا دو prop از Board به Square میفرستیم: value
و onClick
. prop به نام onClick
یک تابع است که Square میتواند هر وقت کلیک شد آن را اجرا کند. تغییرات زیر را در Square ایجاد میکنیم:
- جایگزین کردن
this.state.value
باthis.props.value
در متدrender
مربوط به Square - جایگزین کردن
this.setState()
باthis.props.onClick()
در متدrender
مربوط به Square - حذف
constructor
از Square چون که Square دیگر حالت بازی را در خود ذخیره نمیکند
بعد از این تغییرات، کد کامپوننت Square شبیه به این میباشد:
class Square extends React.Component { render() { return (
<button
className="square"
onClick={() => this.props.onClick()} >
{this.props.value} </button>
);
}
}
وقتی که یک Square کلیک شد تابع onClick
که از Board میآید، صدا زده میشود. در اینجا مروری بر چگونگی بدست آمدن این رخداد میکنیم:
- prop به نام
onClick
در کامپوننت<button>
که از پیش تعریف شده در DOM است برای ریاکت به این معناست که یک شنونده برای رویداد کلیک (click event listener) تعریف کند. - وقتی که دکمه کلیک شد، ریاکت کنترل کنندهی رویداد
onClick
که در متدrender()
کامپوننت Square تعریف شده را صدا میزند. - کنترل کنندهی رویداد در زمان کلیک
this.props.onClick()
را صدا میزند. این prop از طرف Board تعریف شده بود. - از آنجایی که کامپوننت Board
onClick={() => this.handleClick(i)}
را به هر Square میفرستد، Square دستورthis.handleClick(i)
برای Board را در هنگام کلیک شدنش اجرا میکند. - چون هنوز متد
handleClick()
را نساختهایم، کد ما کرش خواهد کرد و اگر روی یک مربع کلیک کنید بر روی صفحه خطایی شبیه به “this.handleClick is not a function” پدیدار میشود.
نکته
المنت
<button>
به دلیل اینکه یک کامپوننت از پیش ساخته شده (built-in) در DOM است، صفتonClick
برای ریاکت معنای ویژهای دارد (که یک کنترل کنندهی رویداد ایجاد برای کلیک ایجاد میکند). اما در کامپوننتهای سفارشی مثل Square، نامگذاریاش دست شماست. ما میتوانیم هر اسمی را به prop به اسمonClick
یا متدhandleClick
بدهیم و کاملا همان کار را انجام دهد. در ریاکت مرسوم است که برای propهایی که نمایندهی یک رویداد هستند ازon[رویداد]
و برای توابعی که به یک رویداد رسیدگی میکنند ازhandle[رویداد]
استفاده میشود.
همان طور که گفته شد اگر روی Square کلیک کنید به دلیل نبود متد handleClick
، خطایی دریافت خواهید کرد. حالا زمان آن رسیده که آن را اضافه میکنیم:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
بعد از انجام این تغییرات شما دوباره قادر خواهید بود با کلیک بر روی هر مربع، آنها را پر کنید، شبیه به چیزی که قبلا داشتیم ولی با این تفاوت که این بار state به جای Square در Board ذخیره میشود و وقتی که state کامپوننت Board تغییر میکند، همهی کامپوننتهای Square به صورت اتوماتیک دوباره رندر میشوند. نگهداری state همهی مربعها در Board به ما اجازه میدهد که بعدا بتوانیم برنده را مشخص کنیم.
از آن جایی که کامپوننتهای Square دیگر state نگهداری نمیکنند، در زمان نیاز مقدارها را از Board گرفته و در زمان کلیک به Board اطلاع میدهند. در اصطلاح ریاکت به Square کامپوننت کنترلشده میگویند. Board کنترل کامل بر روی آنها دارد.
به متد handleClick
نگاه کنید. ما با فراخوانی متد .slice()
یک کپی از آرایهی squares
درست میکنیم تا آن را به جای آرایه فعلی تغییر دهیم. در بخش بعدی دلیل این کار را توضیح خواهیم داد.
چرا تغییرناپذیری (Immutability) مهم است؟
در مثال قبلی دیدید که به جای تغییر دادن مستقیم آرایهی squares
پیشنهاد دادیم با استفاده از متد .slice()
یک کپی از آن تهیه کرده و بعد از انجام تغییرات آن را با متغیر اصلی تعویض کنید. در این بخش به تغییرناپذیری (Immutability) و ضرورت و برتری انجام این کار میپردازیم.
به طور کلی دو روش برای تغییر مقدار یک داده وجود دارد. روش اول این است که مقادیر داده را به صورت مسقیم دگرگون کنیم تا داده تغییر (mutate) کند. روش دوم این است که داده را با یک کپی جدید که دارای تغییرات دلخواه است تعویض کنیم.
تغییر داده با Mutation (تغییر مستقیم)
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// اکنون مقدار متغیر تغییر کرده است
تغییر داده بدون Mutation (تغییر به وسیله تعویض)
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// اکنون متغیر اصلی تغییر نکرده بلکه متغیر جدید از روی اصلی کپی و بعد تغییر پیدا کرده است
// اگر از سینتکس object spread استفاده میکنید میتوانید اینطور نیز بنویسید
// var newPlayer = {...player, score: 2};
همانطور که مشاهده میکنید نتیجه هر دو یکسان است اما با انجام ندادن mutation و تغییر داده به صورت مستقیم چندین مزیت را که در زیر شرح داده شده است، بدست می آوریم.
ساخت امکانات پیچیده آسان میشود
تغییرناپذیری (Immutability) پیادهسازی ویژگیهای پیچیده را بسیار سادهتر میکند. بعداً در این آموزش، ما یک ویژگی “سفر در زمان” را پیادهسازی میکنیم که به ما امکان میدهد تاریخچه بازی tic-tac-toe را مرور کرده و به حرکتهای قبلی “بازگشت” بزنیم. این قابلیت مختص بازیها نیست - توانایی باطلکردن و دوباره انجامدادن برخی از اقدامات یک الزام رایج در برنامههاست. اجتناب از تغییر مستقیم دادهها (Mutation) به ما اجازه میدهد نسخههای قبلی تاریخ بازی را دست نخورده نگهداریم و بعداً از آنها استفاده کنیم.
تشخیص تغییرات
تشخیص تغییرات در اشیایی که صورت مستقیم تغییر میکند بسیار سخت است (منظور از اشیاء همان objects در برنامهنویسی شیگراست). در این روش نیاز است شئ خود را با کپیهای نسخهی قبلی خودش و کل درخت شئ مقایسه شود. [برای مثال باید تمام propertiesهای دو شئ جدید و قدیم به صورت درختی (چون هر property میتواند خود دربرگیرنده چند property دیگر باشد) هم مقایسه شوند].
اما تشخیص تغییرات در اشیاء تغییرناپذیر به صورت قابل توجهی سادهتر است. اگر آدرس حافظهای که شئ مورد نظرمان در آن قرار دارد با نسخهی قبلی تفاوت داشته باشد، پس آن تغییر کرده.
تعیین زمان رندر دوباره برای ریاکت
فایده اصلی استفاده از Immutability امکان ساخت pure components است. زیرا که تشخیص تغییرات در این نوع بسیار ساده است. ریاکت میتواند زمانی که تغییری ایجاد شد آن را شناسایی کرده و کامپوننتهای دچار تغییر را دوباره رندر میکند.
شما میتوانید دربارهی متد shouldComponentUpdate()
و چگونگی ساخت pure components از اینجا اطلاعات کسب کنید. [همان طور که گفته شد تشخیص تغییرات در دادههای Mutable بسیار سخت است و باعث کندی صفحه میشود. بنابر این ریاکت همیشه stateها را Immutable فرض میکند. به همین دلیل اگر کد بالا را به صورت mutable تغییر میدادیم state آن تغییر میکرد اما ریاکت متوجه آن نمیشد و صفحه را دوباره رندر نمیکرد.]
کامپوننتهای تابعی
اکنون Square را به یک کامپوننت تابعی (Function Component) تبدیل میکنیم.
در ریاکت، کامپوننت تابعی راهی ساده برای ساخت کامپونننتهای بدون state و آنهایی که تنها متد render
دارد است. به صورت خلاصه برای ساخت کامپوننتهای ساده و مختصر از کامپونت تابعی استفاده میکنیم. تنها کافیست به جای ساخت کلاسی که از React.Components
ارث میبرد یک تابع بنویسیم که props
را در ورودی دریافت و کد نیاز به رندر را برگرداند. نوشتن کامپوننت تابعی بسیار سادهتر از کلاسهاست و کامپوننتهای زیادی را میتوان به این صورت نوشت.
کلاس Square را با این تابع جایگزین کنید:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
توجه داشته باشید که در این تبدیل نیاز است تمام this.props
ها را با props
جایگزین کنیم.
نکته
دقت کنید که وقتی ما Square را به یک کامپوننت تابعی تبدیل کردیم، همچنین عبارت
onClick={() => this.props.onClick()}
را بهonClick={props.onClick}
تغییر دادیم. (به نبود پرانتز در هر دو طرف دقت کنید)
چرخش نوبت
ما اکنون نیاز داریم نقضی واضح را در بازی را بر طرف کنیم: هیچگاه “O”ها روی تخته بازی مشخص نمیشوند.
اولین حرکت را به صورت پیشفرض “X” قرار میدهیم. این کار میتواند با مقدار دادن اولیه یک state انجام شود. پس آن را در constructor کلاس Board قرار میدهیم.
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
هر وقت که مخاطب حرکتی را انجام میدهد، متغیر xIsNext
(که از نوع boolean است) وارون میگردد تا مشخص کند نوبت نفر بعدیست. بعد از آن state بازی سیو میشود. پس این تغییرات را در متد handleClick
کلاس Board انجام میدهیم تا در زمان کلیک آن را وارون کند:
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({
squares: squares,
xIsNext: !this.state.xIsNext, });
}
با این تغییرات، با هر حرکت نوبت “X”ها و “O”ها تغییر میکند و عوض میشود. میتوانید امتحان کنید!
بیایید متن “status” را هم در متد render
بورد عوض کنیم تا به کاربر نشان دهد که حرکت بعدی نوبت کیست:
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// بقیه تغییری نمیکند
بعد از این تغییرات، کامپوننت بوردتان باید به این صورت باشد:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
handleClick(i) {
const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
تعیین برنده
اکنون که نشان میدهیم نوبت کدام بازیکن است ، وقت آن است زمانی که کسی برنده میشود و دیگر نوبت کسی نیست را نیز نشان دهیم. این تابع کمکی را در آخر فایلتان اضافه کنید:
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
با توجه به آرایه ای از 9 مربع ، این تابع برنده را بررسی می کند و در صورت لزوم ‘X’، ‘O’، یا null را برمی گرداند.
برای چک کردن برنده(و اینکه اصلا کسی برنده شده یا نه) در متد render
بُرد calculateWinner(squares)
را صدا میزنیم. اگر کسی برنده شده باشد، میتوانیم متنی مشابه “Winner: X” و یا “Winner: O” روی صفحه نمایش دهیم. پس معرف status
را در متد render
کلاس Board با این کد جایگزین میکنیم:
render() {
const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
// بقیه تغییری نمیکند
حالا میتوانیم تغییراتی را در متد handleClick کلاس Board ایجاد کنیم تا با نادیده گرفتن یک کلیک در صورت برنده شدن بازی توسط یکی از دو بازیکن یا اگر مربع در حال حاضر پر شده باشد، زود بازگردد:
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
تبریک! شما در حال حاضر یک بازی tic-tac-toe دارید. و همزمان اصول اولیه React را نیز آموختهاید. بنابراین احتمالاً شما برنده واقعی اینجا هستید.
اضافه کردن قابلیت ماشین زمان
به عنوان آخرین تمرین، بیایید “بازگشت به عقب در زمان” به حرکتهای قبلی را در بازی پیاده سازی کنیم.
ذخیرهی تاریخچهی حرکتها
اگر در گذشته آرایهی squares
را به صورت تغییریافته (Mutation) تعریف کرده بودیم، اکنون پیادهسازی ماشین زمان بسیار سخت میشد.
با این وجود ما بعد از هر حرکت در بازی ابتدا با استفاده slice()
یک کپی از آرایهی squares
گرفته و با آن به صورت تغییرناپذیر (Immutable) رفتار میکردیم. این کار اجازه میدهد تمامی نسخههای قبلی آن را ذخیره کنیم و بتوانیم در بین گامهای برداشته شده در بازی حرکت کنیم.
تمام نسخههای قبلی squares
را در آرایهای دیگر به نام history
ذخیره میکنیم. پس آرایهی history
تمام state های کلاس Board را از ابتدا تا آخرین حرکت را دربر خواهد داشت و چیزی شبیه به این خواهد شد:
history = [
// قبل از اولین حرکت
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// بعد از یک حرکت
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// بعد از دومین حرکت
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
اکنون باید تصمیم بگیریم که آرایهی history
در state کدام کامپوننت باید قرار گیرد.
بالا بردن دوبارهی state
ما نیاز داریم کامپوننت سطح بالای Game لیست تمامی حرکتهای گذشته را نمایش دهد. برای این کار او نیاز به دسترسی به آرایهی history
نیاز دارد. پس بهتر است آن را در خود Game قرار دهیم.
قرار دادن history
به صورت state در کامپوننت Game به ما اجازه میدهد تا state به نام squares
را از فرزندش یعنی Board حذف کنیم. دقیقا شبیه به بخش “بالا بردن state” که از Square به Board انتقال داده بودیم، الآن آن را از Board به کامپوننت بالا رده یعنی Game انتقال میدهیم. این کار به Game اجازه میدهد تا کنترل کامل بر روی داده Board داشته باشد و به Board بگوید تا حرکتهای قبلی را از history
رندر کند.
ابتدا نیاز داریم state اولیه را در constructor کامپوننت Game مقدار دهی کنیم:
class Game extends React.Component {
constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; }
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
در مرحلهی بعد کاری میکنیم تا prop های به نام squares
و onClick
را کامپوننت Board از کامپوننت Game دریافت کند. از آنجا که ما یک کنترل کنندهی کلیک برای تعداد زیادی Square داریم، نیاز است تا مکان هر یک از آنها را نیز به کنترلر onClick
بفرستیم تا نشان دهد که کدام مربع کلیک شده است. در اینجا چند مرحله برای تغییر دادن کامپونت Board لازم است:
- حذف متد
constructor
از Board - جایگزین کردن
this.state.squares[i]
باthis.props.squares[i]
- جایگزین کردن
this.handleClick(i)
باthis.props.onClick(i)
در متدrenderSquare
بورد.
هماکنون کلاس Board باید به این صورت باشد:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
همچنین باید در متد render
کامپوننت Game تغییراتی بدهیم تا از آخرین عضو متغیر history
برای مشخص کردن و نمایش حالت بازی استفاده کند:
render() {
const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
<div className="game">
<div className="game-board">
<Board squares={current.squares} onClick={(i) => this.handleClick(i)} /> </div>
<div className="game-info">
<div>{status}</div> <ol>{/* TODO */}</ol>
</div>
</div>
);
}
از آنجایی که Game حالت بازی را رندر میکند. میتوانیم کدهای اضافی را از متد render
کلاس Board پاک کنیم. بعد از پاکسازی، تابع render
کلاس Board باید چیزی شبیه به این شود:
render() { return ( <div> <div className="board-row"> {this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
در نهایت، ما نیاز داریم متد handleClick
را از کامپوننت Board به کامپوننت Game منتقل کنیم. همچنین نیاز داریم کمی آن را تغییر دهیم زیرا که ساختار state کامپوننتGame کمی متفاوت است. در متد handleClick
کامپوننت Game ورودیهای جدید تاریخچه را به history
الحاق میکنیم.
handleClick(i) {
const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{ squares: squares, }]), xIsNext: !this.state.xIsNext,
});
}
نکته
برخلاف متد
push()
آرایهها که احتمالا با آن بیشتر آشنایی دارید، متدconcat()
آرایهی اصلی را تغییر نمیدهد، پس ما آن را بهpush()
ترجیح میدهیم.
در اینجا کامپوننت Board تنها به متدهای renderSquare
و render
نیاز دارد. state بازی و متد handleClick
باید در کامپوننت Game قرار گرفته باشند.
نمایش حرکتهای قبلی
از آنجا که ما تاریخچه بازی tic-tac-toe را ضبط می کنیم، اکنون می توانیم آن را به عنوان لیستی از حرکات گذشته به بازیکن نشان دهیم.
پیشتر متوجه شدیم که المنتهای ریاکت، اشیاء (object) جاوا اسکریپت درجه یک هستند. ما می توانیم آنها را در برنامه های خود منتقل کنیم. برای رندر چندین آیتم در ریاکت، می توانیم از آرایه ای از المنتهای ریاکت استفاده کنیم.
در جاوا اسکریپت ، آرایه ها دارای متد map()
هستند که معمولاً برای نگاشت دادهها به دادههای دیگر استفاده میشود ، به عنوان مثال:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
با استفاده از متد map
، میتوانیم سابقه حرکت خود را به المنتهای ریاکت که به صورت دکمههایی روی صفحه نمایانگرهستند نگاشت کنیم، و لیستی از دکمهها را برای “پرش” به حرکتهای قبلی نمایش دهیم.
بیایید در متد render
کامپوننت Game، بر روی history
عملیات map
را اجرا کنیم:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol> </div>
</div>
);
}
همانطور که آرایه history
مرور می کنیم، متغیر step
به مقدار فعلی المنت history
و move
به شاخص فعلی المنت history
اشاره دارد. ما در اینجا فقط به move
علاقه مندیم، بنابراین step
به چیزی اختصاص داده نمی شود.
بدین صورت، برای هر حرکت در بازی یک ایتم لیست <li>
درست میشود که یک دکمهی <button>
را دربر دارد. کنترلر onClick
این دکمه، متد this.justTo()
را صدا میزند که هنوز آن را پیادهسازی نکردهایم. ما در حال حاضر باید لیستی از حرکتهای بازی و این هشدار را در developer tools مرورگرمان ببینیم:
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.
حال بیایید ببینیم دلیل این هشدار چیست.
برگزیدن یک کلید (key)
وقتی لیستی را رندر میکنیم، ریاکت اطلاعاتی در مورد هر ایتم لیست ارائه شده ذخیره میکند. وقتی لیستی را بهروز میکنیم، ریاکت باید تعیین کند که چهچیزی تغییر کرده است. ما میتوانستیم آیتمهای لیست را اضافه، حذف، مرتب یا به روز کرده باشیم.
فرض کنید این لیست را:
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
به این لیست تبدیل کنیم:
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
علاوه بر شمارشهای بهروزشده، انسانی که این مطلب را بخواند احتمالاً میگوید که ما ترتیب Alexa و Ben را عوض کرده و Claudia را بین Alexa و Ben قرار دادهایم. اما از آن جایی که ریاکت که یک برنامهی کامپیوتری است نمیتواند بفهمد چه چیزی اضافه شدهاست. پس نیاز داریم برای این که هر عضو از دیگری متمایز شود، برایشان یک ویژگی key تعریف کنیم که مخصوص خودش باشد (منحصر به فرد باشد). در این جا یک گزینه میتواند استفاده از مقادیر alexa
, ben
و یا claudia
باشد. اگر این اطلاعات از یک دیتابیس است میتوانیم آیدی (شناسه) آنها را به عنوان key استفاده کنیم.
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
هنگامی که یک لیست دوباره رندر میشود، ریاکت کلید (key) هر آیتم لیست را میگیرد و آیتمهای لیست قبلی را برای یک کلید منطبق جستجو میکند. اگر لیست فعلی دارای کلیدی است که قبلاً وجود نداشت، ریاکت یک کامپوننت ایجاد میکند. اگر لیست فعلی فاقد کلیدی باشد که در لیست قبلی وجود داشت، ریاکت کامپوننت قبلی را از بین میبرد. اگر دو کلید مطابقت داشته باشند، کامپوننت مربوطه منتقل میشود. کلیدها در مورد هویت هر کامپوننت به ریاکت میگویند که به ریاکت اجازه میدهد بین رندرهای مجدد state را حفظ کند. اگر کلید یک کامپوننت تغییر کند ، کامپوننت از بین میرود و با یک state جدید دوباره ایجاد میشود.
key
یک property خاص و رزرو شده در ریاکت است (به علاوهی ref
که یک ویژگی پیشرفتهست). هنگامی که یک المنت ایجاد می شود، ریاکت property به نام key
را استخراج میکند و کلید را مستقیماً روی المنت بازگشتی ذخیره می کند. با اینکه key
به نظر میرسد که متعلق به props
است، نمی توان با استفاده از this.props.key
به key
اشاره کرد. ریاکت به طور خودکار از key
برای تصمیم گیری در مورد بهروزرسانی کامپوننتها استفاده میکند. یک کامپوننت نمیتواند در مورد key
خود سوال کند و از آن مطلع شود.
شدیدا توصیه میشود که اگر با لیستهای داینامیک (تغییر میکنند و جا به جا میشوند) کار میکنید از key مناسب برای هر لیست استفاده کنید. اگر کلید مناسب ندارید، ممکن است بخواهید داده های خود را تجدید ساختار کنید تا این کار را انجام دهید.
اگر در لیستها ویژگی key را مقدار دهی نکنید، ریاکت هشداری خواهد داد و به صورت پیشفرض از مکان (اندیس آرایه) هر عضو آن لیست به عنوان key استفاده میکند. و این کار میتواند بسیار در جابهجا، اضافه یا حذف عضو مشکلساز باشد. توجه داشته باشید که هرچند که میتوانیم با قرار دادن key={i}
(i اندیس آرایه است) هشدار از بین ببریم اما مشکلات یکسان مثل اندیس آرایه همچنان باقیست و در بیشتر موارد توصیه نمیشود.
نیازی به خاص بودن کلیدها در کل به صورت جهانی نیست و تنها خاص بودن بین برادر/خواهرهایشان کفایت میکند.
پیادهسازی ماشین زمان
در تاریخچهی بازی، هر حرکت گذشته یک شناسهی منحصر به فرد دارد و آن نیز مرتبهی آن حرکت است. حرکتها هرگز دوباره مرتب نمیشوند، حذف نمیشوند یا در وسط قرار داده نمیشوند، پس استفاده از مرتبهی حرکت به عنوان key امن است و مشکلی ندارد.
برای اینکه هشدار نبود key از بین برود در متد render
کامپوننت Game، میتوانیم key را برابر {move}
بگذاریم:
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
با کلیک بر روی هر یک از دکمهها، خطایی به معنای نبود متد jumpTo
ظاهر میشود. بعدا این متد را نیز خواهیم ساخت اما قبل از آن نیاز داریم تا یک state به نام stepNumber
برای نمایش تعداد گامهایی که حرکت انجام گرفته تعریف کنیم.
ابتدا، state به صورت stepNumber: 0
را به منظور دادن مقدار اولیه در constructor
کامپوننت Game اضافه میکنیم.
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, xIsNext: true,
};
}
در مرحلهی بعد متد jumpTo
را به منظور بهروزرسانی کردن stepNumber
تعریف میکنیم. و همچنین برای اینکه نوبت افراد بههم نخورد، اگر گام مورد نظر زوج بود متغیر xIsNext
برابر true میکنیم.
handleClick(i) {
// this method has not changed
}
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); }
render() {
// این متد تغییری نمیکند
}
توجه داشته باشید که در متد jumpTo
، ویژگی history
مربوط به حالت (state) را بهروزرسانی نکردهایم. دلیل آن این است که بهروزرسانیهای حالت (state) ادغام میشوند یا به عبارت سادهتر React فقط ویژگیهای ذکر شده در متد setState
را بهروزرسانی میکند و حالت (state) باقیمانده را به همان صورت باقی میگذارد. برای اطلاعات بیشتر به مستندات مراجعه کنید.
حال در متد handleClick
کلاس Game که موقع کلید روی مربعها فعال میشود، تغییراتی میدهیم.
state به نام stepNumber
که ما اضافه کردهایم منعکسکننده حرکت نشان دادهشده در حال حاضر به کاربر است. پس از انجام یک حرکت جدید ، باید stepNumber
را با افزودن stepNumber: history.length
به عنوان بخشی از آرگومان this.setState
به روز کنیم. این اطمینان میدهد که ما بعد از انجام یک حرکت جدید ، همان حرکت را نشان نمیدهیم.
ما همچنین به جای خواندن متغیر this.state.history
آن را با this.state.history.slice(0, this.state.stepNumber + 1)
عوض میکنیم. این تضمین میکند که اگر “به عقب برگردیم” و سپس از آن نقطه حرکت جدیدی انجام دهیم ، تمام تاریخ “آینده” را که اکنون نادرست شدهاست دور میاندازیم.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length, xIsNext: !this.state.xIsNext,
});
}
در نهایت ، ما متد render
کامپوننت Game را از اینکه همیشه آخرین حرکت را رندر کند به رندر حرکت انتخاب شده فعلی مطابق stepNumber تغییر می دهیم:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
// بقیه بدون تغییر باقی میماند
اگر روی هر مرحله از تاریخچه بازی کلیک کنیم، تخته tic-tac-toe باید فوراً به روز شود تا نشان دهد که صفحه بعد از آن مرحله چگونه بوده است.
جمعبندی
تبریک! یک بازی tic-tac-toe ساختید که:
- به کاربر اجازه میدهد tic-tac-toe بازی کند،
- برنده را مشخص و نمایش میدهد،
- روند بازی را ذخیره میکند،
- میگذارد کاربر تاریخچهی بازی را بررسی و به گامهای انجام داده برگردد.
بسیار خب، امیدواریم درک خوبی از نحوهی کارکرد با ریاکت به دست آورده باشید.
نتیجه نهایی را اینجا ببینید: نتیجه نهایی.
اگر وقت اضافه دارید و یا میخواهید مهارت خودتان را در ریاکت تمرین کنید، در اینجا چند ایده برای بهبودهایی که می توانید در بازی tic-tac-toe ایجاد کنید به ترتیب افزایش دشواری ذکر شده است:
- نمایش مکان حرکت انجام شده در لیست تاریچهی حرکات
- درشت شدن (Bold) شدن حرکت جاری در لیست حرکات
- بازنویسی Board تا از دو حلقه (مثل for) به جای نوشتن دستی همهی مربعها استفاده کند.
- درست کردن یک دکمه که حرکات را به صورت صعودی یا نزولی مرتب کند.
- وقتی که کسی برنده شد، سه مربع که باعث این برد بودند را برجسته کند.
- هنگامی که هیچ کس برنده نمیشود ، پیامی در مورد نتیجه تساوی نشان دهید.
در طول این آموزش با مفاهیم اصلی ریاکت همچون المنتها، کامپوننتها، props و state آشنا شدید. برای اطلاع بیشتر درباره هر یک از موضوعات میتوانید به مستندات ریاکت. برای اطلاع بیشتر دربارهی تعریف کامپوننتها مرجع API برای React.Component
را بررسی کنید.