<button onClick={this.handleReset} className="btn btn-primary btn-sm m-2" > Reset </button>
handleReset = () => { const counters = this.state.counters.map(counter => { counter.value = 0; return counter; }); this.setState({ counters }); // = this.setState({ counters: counters }); };
اکنون پس از ذخیره سازی فایل counters.jsx و بارگذاری مجدد برنامه در مرورگر، هرچقدر بر روی دکمهی Reset کلیک کنیم ... اتفاقی رخ نمیدهد! حتی اگر به افزونهی React developer tools نیز مراجعه کنیم، مشاهده خواهیم کرد که عمل تنظیم value به صفر، در تک تک کامپوننتهای شمارشگر، به درستی صورت گرفتهاست؛ اما تغییرات به DOM اصلی منعکس نشدهاند:
البته اگر به همین تصویر دقت کنید، هنوز مقدار count، در state آن 4 است. علت اینجا است که هر کدام از Counterها دارای local state خاص خودشان هستند و در آنها، مقدار count به صورت زیر مقدار دهی شدهاست که در آن تغییرات بعدی این this.props.value، متصل به count نیست و count، فقط یکبار مقدار دهی میشود:
class Counter extends Component { state = { count: this.props.counter.value };
حذف Local state
اکنون میخواهیم در کامپوننت Counter، قسمت local state آنرا به طور کامل حذف کرده و تنها از this.props جهت دریافت اطلاعاتی که نیاز دارد، استفاده کنیم. به این نوع کامپوننتها، «Controlled component» نیز میگویند. یک کامپوننت کنترل شده دارای local state خاص خودش نیست و تمام دادههای دریافتی را از طریق this.props دریافت میکند و هر زمانیکه قرار است دادهای تغییر کند، رخدادی را به والد خود صادر میکند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل میشود.
برای پیاده سازی این مفهوم، ابتدا خاصیت state کامپوننت Counter را حذف میکنیم. سپس تمام ارجاعات به this.state را در این کامپوننت یافته و آنها را تغییر میدهیم. اولین ارجاع، در متد handleIncrement به صورت this.state.count تعریف شدهاست:
handleIncrement = () => { this.setState({ count: this.state.count + 1 }); };
<button onClick={() => this.props.onIncrement(this.props.counter)} className="btn btn-secondary btn-sm" > Increment </button>
getBadgeClasses() { let classes = "badge m-2 badge-"; classes += this.props.counter.value === 0 ? "warning" : "primary"; return classes; } formatCount() { const { value } = this.props.counter; // Object Destructuring return value === 0 ? "Zero" : value; }
در ادامه به کامپوننت Counters مراجعه کرده و متد رویدادگردانی را جهت پاسخگویی به رخداد onIncrement رسیدهی از کامپوننتهای فرزند، تعریف میکنیم:
handleIncrement = counter => { console.log("handleIncrement", counter); };
<Counter key={counter.id} counter={counter} onDelete={this.handleDelete} onIncrement={this.handleIncrement} />
پیاده سازی کامل متد handleIncrement اینبار به صورت زیر است:
handleIncrement = counter => { console.log("handleIncrement", counter); const counters = [...this.state.counters]; // cloning an array const index = counters.indexOf(counter); counters[index] = { ...counter }; // cloning an object counters[index].value++; console.log("this.state.counters", this.state.counters[index]); this.setState({ counters }); };
تا اینجا اگر برنامه را ذخیره کرده و منتظر به روز رسانی آن در مرورگر شویم، با کلیک بر روی Reset، تمام کامپوننتها با هر وضعیتی که پیشتر داشته باشند، به حالت اول خود باز میگردند:
همگام سازی چندین کامپوننت با هم زمانیکه رابطهی والد و فرزندی بین آنها وجود ندارد
در ادامه میخواهیم یک منوی راهبری (یا همان NavBar در بوت استرپ) را به بالای صفحه اضافه کنیم و در آن جمع کل تعداد Counterهای رندر شده را نمایش دهیم؛ مانند نمایش تعداد آیتمهای انتخاب شدهی توسط یک کاربر، در یک سبد خرید. برای پیاده سازی آن، درخت کامپوننتهای React را مطابق شکل فوق تغییر میدهیم. یعنی مجددا کامپوننت App را در به عنوان کامپوننت ریشهای انتخاب کرده که سایر کامپوننتها از آن مشتق میشوند و همچنین کامپوننت مجزای NavBar را نیز اضافه خواهیم کرد.
برای این منظور به index.js مراجعه کرده و مجددا کامپوننت App را که غیرفعال کرده بودیم و بجای آن Counters را نمایش میدادیم، اضافه میکنیم:
import App from "./App"; ReactDOM.render(<App />, document.getElementById("root"));
سپس کامپوننت جدید NavBar را توسط فایل جدید src\components\navbar.jsx اضافه میکنیم تا منوی راهبری سایت را نمایش دهد:
import React, { Component } from "react"; class NavBar extends Component { render() { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar </a> </nav> ); } } export default NavBar;
اکنون به App.js مراجعه کرده و متد render آنرا جهت نمایش درخت کامپوننتهایی که مشاهده کردید، تکمیل میکنیم:
import "./App.css"; import React from "react"; import Counters from "./components/counters"; import NavBar from "./components/navbar"; function App() { return ( <React.Fragment> <NavBar /> <main className="container"> <Counters /> </main> </React.Fragment> ); } export default App;
تا اینجا اگر برنامه را ذخیره کنیم تا در مرورگر بارگذاری مجدد شود، چنین شکلی حاصل شدهاست:
اکنون میخواهیم تعداد کامپوننتهای شمارشگر را در navbar نمایش دهیم. پیشتر state کامپوننت Counters را توسط props، به کامپوننتهای Counter رندر شدهی توسط آن انتقال دادیم. استفادهی از این ویژگی به دلیل وجود رابطهی والد و فرزندی بین این کامپوننتها میسر شد. اما همانطور که در تصویر درخت کامپوننتهای جدید تشکیل شده مشاهده میکنید، رابطهی والد و فرزندی بین دو کامپوننت Counters و NavBar وجود ندارد. بنابراین اکنون این سؤال مطرح میشود که چگونه باید تعداد کل شمارشگرهای کامپوننت Counters را به کامپوننت NavBar، برای نمایش آنها انتقال داد؟ در یک چنین حالتهایی که رابطهی والد و فرزندی بین کامپوننتها وجود ندارد و میخواهیم آنها را همگام سازی کنیم و دادههایی را بین آنها به اشتراک بگذاریم، باید state را به یک سطح بالاتر انتقال داد. یعنی در این مثال باید state کامپوننت Counters را به والد آن که اکنون کامپوننت App است، منتقل کرد. پس از آن چون هر دو کامپوننت NavBar و Counters، از کامپوننت App مشتق میشوند، اکنون میتوان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت.
انتقال state به یک سطح بالاتر
برای انتقال state به یک سطح بالاتر، به کامپوننت Counters مراجعه کرده و خاصیت state آنرا به همراه تمامی متدهایی که آنرا تغییر میدهند و از آن استفاده میکنند، انتخاب و cut میکنیم. سپس به کامپوننت App مراجعه کرده و آنها را در اینجا paste میکنیم. یعنی خاصیت state و متدهای handleDelete، handleReset و handleIncrement را از کامپوننت Counters به کامپوننت App منتقل میکنیم. این مرحلهی اول است. سپس نیاز است به کامپوننت Counters مراجعه کرده و ارجاعات به state و متدهای یاد شده را توسط props اصلاح میکنیم. برای این منظور ابتدا باید این props را در کامپوننت App مقدار دهی کنیم تا بتوانیم آنها را در کامپوننت Counters بخوانیم؛ یعنی متد render کامپوننت App، تمام این خواص و متدها را باید به صورت ویژگیهایی به تعریف المان Counters اضافه کند تا خاصیت props آن بتواند به آنها دسترسی داشته باشد:
render() { return ( <React.Fragment> <NavBar /> <main className="container"> <Counters counters={this.state.counters} onReset={this.handleReset} onIncrement={this.handleIncrement} onDelete={this.handleDelete} /> </main> </React.Fragment> ); }
پس از این تعاریف میتوانیم به کامپوننت Counters بازگشته و ارجاعات فوق را توسط خاصیت props، در متد render آن اصلاح کنیم:
render() { return ( <div> <button onClick={this.props.onReset} className="btn btn-primary btn-sm m-2" > Reset </button> {this.props.counters.map(counter => ( <Counter key={counter.id} counter={counter} onDelete={this.props.onDelete} onIncrement={this.props.onIncrement} /> ))} </div> ); }
پس از این نقل و انتقالات، اکنون میتوانیم تعداد counters را در NavBar نمایش دهیم. برای این منظور ابتدا در کامپوننت App، به همان روشی که ویژگی counters={this.state.counters} را به تعریف المان Counters اضافه کردیم، شبیه به همین کار را برای کامپوننت NavBar نیز میتوانیم انجام دهیم تا از طریق خاصیت props آن قابل دسترسی شود و یا حتی میتوان به صورت زیر، تنها جمع کل را به آن کامپوننت ارسال کرد:
<NavBar totalCounters={this.state.counters.filter(c => c.value > 0).length} />
سپس در کامپوننت NavBar، عدد totalCounters فوق را که به تعداد کامپوننتهایی که مقدار value آنها بیشتر از صفر است، اشاره میکند، از طریق خاصیت props خوانده و نمایش میدهیم:
class NavBar extends Component { render() { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar{" "} <span className="badge badge-pill badge-secondary"> {this.props.totalCounters} </span> </a> </nav> ); } }
کامپوننتهای بدون حالت تابعی
اگر به کدهای کامپوننت NavBar دقت کنیم، تنها یک تک متد render در آن ذکر شدهاست و تمام اطلاعات مورد نیاز آن نیز از طریق props تامین میشود و دارای state و یا هیچ رویدادگردانی نیست. یک چنین کامپوننتی را میتوان به یک «Stateless Functional Component» تبدیل کرد؛ کامپوننتهای بدون حالت تابعی. در اینجا بجای اینکه از یک کلاس برای تعریف کامپوننت استفاده شود، میتوان از یک function استفاده کرد (به همین جهت به آن functional میگویند). احتمالا نمونهی آنرا با کامپوننت App پیشفرض قالب create-react-app نیز مشاهده کردهاید که در آن فقط یک ()function App وجود دارد. البته در کدهای فوق چون نیاز به ذکر state، در کامپوننت App وجود داشت، آنرا از حالت تابعی، به حالت کلاس استاندارد کامپوننت، تبدیل کردیم.
اگر بخواهیم کامپوننت بدون حالت NavBar را نیز تابعی کنیم، میتوان به صورت زیر عمل کرد:
import React from "react"; // Stateless Functional Component const NavBar = props => { return ( <nav className="navbar navbar-light bg-light"> <a className="navbar-brand" href="#"> Navbar{" "} <span className="badge badge-pill badge-secondary"> {props.totalCounters} </span> </a> </nav> ); }; export default NavBar;
پیشتر در کامپوننت NavBar از شیء this استفاده شده بود. این روش تنها با کلاسهای استاندارد کامپوننت کار میکند. در اینجا باید props را به عنوان پارامتر متد دریافت (همانند مثال فوق) و سپس از آن استفاده کرد.
البته لازم به ذکر است که انتخاب بین «کامپوننتهای بدون حالت تابعی» و یک کامپوننت معمولی تعریف شدهی توسط کلاسها، صرفا یک انتخاب شخصی است.
یک نکته: امکان Destructuring Arguments نیز در اینجا وجود دارد. یعنی بجای اینکه یکبار props را به عنوان پارامتر دریافت کرد و سپس توسط آن به خاصیت totalCounters دسترسی یافت، میتوان نوشت:
const NavBar = ({ totalCounters }) => {
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-08.zip