در این قسمت می خواهیم Router را طوری تنظیم کنیم که با کلیک روی عنوان یک پست، محتوای آن پست بارگذاری شود. در حال حاضر متدی به نام postSelectedHandler داریم که مسئول مدیریت کلیک روی عناوین پست ها است:
postSelectedHandler = (id) => { this.setState({ selectedPostId: id }); }
مشکل اینجاست که ما دیگر چنین state ای نداریم و چند جلسه ی قبل تمام ساختار پروژه را تغییر دادیم. بنابراین باید آن را دوباره کدنویسی کنیم تا محتوای پست ها را بارگذاری کند. البته قبل از آن باید یک سوال اساسی از خودمان بپرسیم: محتوای پست را کجا نمایش دهیم؟ یک راه، نمایش آن در Blog.js است.
همچنین زمانی که روی یک پست کلیک می کنیم باید به react بگوییم روی کدام پست کلیک کرده ایم تا بداند کدام محتوا را باز کند. اگر یادتان باشد این کار را با استفاده از id انجام می دادیم:
posts = this.state.posts.map(post => { return <Post key={post.id} title={post.title} author={post.author} clicked={() => this.postSelectedHandler(post.id)} />; });
اما زمانی که از Router استفاده می کنیم باید این id را به صورت یک پارامتر پویا در URL ارسال کنیم تا کامپوننت FullPost بداند کدام پست را بارگذاری کند. ما نمی توانیم id هر پست را به صورت دستی وارد کنیم:
<Route path="/1" exact component={Posts} /> <Route path="/2" exact component={Posts} /> <Route path="/3" exact component={Posts} />
این کار شدنی نیست چرا که اصلا نمی توانیم بفهمیم در کل چند پست داریم و حتی اگر تعداد پست ها را بدانیم، هر روزه پست های جدیدی در سایت منتشر می شود. این مسئله یعنی ما باید با انتشار هر پست، سورس کد را ویرایش کنیم. بنابراین راه حل آن ایجاد یک پارامتر پویا در url است:
<Route path="/:id" exact component={Posts} />
در این روش باید علامت دو نقطه را اضافه کرده و سپس یک نام برای پارامتر پویای خود انتخاب کنید (من نام id را انتخاب کرده ام). این مقدار به صورت پویا توسط react جایگزین خواهد شد. در واقع این مقدار باید زمانی ارسال و جایگزین بشود که ما روی یکی از پست ها کلیک کنیم. برای انجام این کار دو روش وجود دارد؛ روش اول قرار دادن کل کامپوننت <Post> (درون فایل Posts.js) درون یک <Link> است. یعنی این قسمت:
<Post key={post.id} title={post.title} author={post.author} clicked={() => this.postSelectedHandler(post.id)} />;
بگذارید نشانتان بدهم. ابتدا <Link> را در فایل Posts.js وارد می کنیم:
import { Link } from 'react-router-dom';
سپس عنصر <Post> را درون آن قرار می دهیم:
if (!this.state.error) { posts = this.state.posts.map(post => { return ( <Link to={'/' + post.id} key={post.id}> <Post title={post.title} author={post.author} clicked={() => this.postSelectedHandler(post.id)} /> </Link>); }); }
در اینجا عنصر <Post> را درون <Link> قرار داده ایم و به to مقدار id را داده ایم. همچنین از آنجایی که درون تابع map هستیم و <Link> عنصر خارجی ما است باید key را از Post گرفته و به <Link> بدهیم.
حالا به Blog.js برگردید و کامپوننت FullPost را وارد پروژه کنید:
import FullPost from './FullPost/FullPost';
و دستور <Route> برای id را به شکل زیر ویرایش کنید:
<Route path="/" exact component={Posts} /> <Route path="/new-post" component={NewPost} /> <Route path="/:id" exact component={FullPost} />
در قسمت component مقدار FullPost را قرار داده ایم تا این کامپوننت بارگذاری شود. حالا اگر به مرورگر برویم و روی یکی از پست ها کلیک کنیم، مقدار id را درون URL میبینیم. مثلا با کلیک روی پست اول به آدرس http://localhost:3000/1 میروید اما به جای محتوای پست، متن !Please select a Post را مشاهده می کنید. آیا می دانید چرا؟
به دلیل اینکه درون فایل FullPost.js چنین کدی داریم:
componentDidUpdate() { if (this.props.id) { if (!this.state.loadedPost || (this.state.loadedPost && this.state.loadedPost.id !== this.props.id)) { axios.get('/posts/' + this.props.id) .then(response => { // console.log(response); this.setState({ loadedPost: response.data }); }); } } }
اما شرط if، یعنی this.props.id، درون این کامپوننت دیگر وجود ندارد. دیگر پارامتر را بدین شکل ارسال نمی کنیم، بلکه آن را در URL قرار می دهیم. اگر یادتان باشد در بحث Routing Props دیدیم که شیء match خصوصیتی به نام params داشت:
باید در فایل FullPost.js تابع componentDidUpdate را به componentDidMount تغییر بدهیم چرا که دیگر این کامپوننت را update (بروز رسانی) نمی کنیم بلکه Mount می شود یعنی از DOM حذف شده یا به آن اضافه می شود. سپس درون componentDidMount مقدار prop ها را console.log می کنیم:
componentDidMount() { console.log(this.props); if (this.props.id) { if (!this.state.loadedPost || (this.state.loadedPost && this.state.loadedPost.id !== this.props.id)) { axios.get('/posts/' + this.props.id) .then(response => { // console.log(response); this.setState({ loadedPost: response.data }); }); } } }
حالا به مرورگر برمی گردیم تا در قسمت console مقدار params را ببینیم:
اسم مقدار درون آن id است چرا که ما آن را id گذاشته بودیم:
<Route path="/:id" exact component={FullPost} />
حالا می توانیم کدهای componentDidMount را بدین شکل تغییر دهیم:
componentDidMount() { console.log(this.props); if (this.props.match.params.id) { if (!this.state.loadedPost || (this.state.loadedPost && this.state.loadedPost.id !== this.props.id)) { axios.get('/posts/' + this.props.match.params.id) .then(response => { // console.log(response); this.setState({ loadedPost: response.data }); }); } } }
در این کد شرط if را تغییر داده ایم (this.props.match.params.id) همچنین برای axios.get مقدار id را از this.props.id به this.props.match.params.id تغییر داده ایم.
در حال حاضر برنامه ی ما مشکل عجیبی دارد. اگر روی Home کلیک کنیم صفحه ی Home می آید اما اگر روی New Post کلیک کنیم، محتوای پست نیز برایمان نمایش داده می شود:
دلیل این اتفاق در فایل Blog.js قابل مشاهده است:
<Route path="/" exact component={Posts} /> <Route path="/new-post" component={NewPost} /> <Route path="/:id" exact component={FullPost} />
از سه دستور <Route> بالا باید به دستور سوم نگاه کنید. این دستور یک پارامتر پویا به نام id دارد که در جلسه ی قبل در مورد آن صحبت کردیم و گفتیم react به صورت خودکار مقدار مناسب را برایش قرار می دهد. مسئله اینجاست که id می تواند هر مقداری داشته باشد، بنابراین new-post نیز می تواند به عنوان مقداری برای id حساب شده و باعث بارگذاری کامپوننت FullPost شود! در واقع اگر Route های ما مطابق با path باشند همگی بارگذاری می شوند.
یکی از راه های حل این موضوع تغییر کد به شکل زیر است:
<Route path="/posts/:id" exact component={FullPost} />
و سپس تصحیح فایل Posts.js به شکل زیر:
posts = this.state.posts.map(post => { return ( <Link to={'/posts/' + post.id} key={post.id}> <Post title={post.title} author={post.author} clicked={() => this.postSelectedHandler(post.id)} /> </Link>); });
یعنی posts/ را به آدرس to اضافه کرده ایم. حالا اگر کدها را تست کنید هیچ مشکلی نخواهیم داشت. اما برخی اوقات می خواهید آدرس ها دقیقا همانطور که هستند باقی بمانند (به دلایلی مثل زیبایی URL یا هر دلیل دیگری). کدها را به حالت اولیه شان برگردانید تا روش دوم را برایتان توضیح دهم.
روش دیگر حل این مشکل این است که به react بگوییم فقط یکی از این Route ها را بارگذاری کن. این کار با استفاده از کامپوننت Switch انجام می شود بنابراین باید ابتدا آن را import کنیم (درون فایل Blog.js):
import { Route, NavLink, Switch } from 'react-router-dom';
حالا باید تمام Route هایمان را درون آن قرار دهیم:
<Switch> <Route path="/" exact component={Posts} /> <Route path="/new-post" component={NewPost} /> <Route path="/:id" exact component={FullPost} /> </Switch>
حالا react می فهمد که فقط اولین Route ای را که با URL منطبق است بارگذاری کرده و بعد از آن دیگر چیزی را بارگذاری نکند. حتی می توانید Route اول را خارج از Switch قرار بدهید:
<Route path="/" exact component={Posts} /> <Switch> <Route path="/new-post" component={NewPost} /> <Route path="/:id" exact component={FullPost} /> </Switch>
کار با این موارد به سلیقه ی شما بستگی دارد. همچنین توجه داشته باشید که اگر جای Route های داخل Switch را تغییر بدهیم دیگر به صفحه ی new-post دسترسی نخواهیم داشت. چرا؟ به دلیل اینکه دستور switch اولین Route مطابق با URL را بارگذاری می کند و گفتیم که مقدار id می تواند هر چیزی باشد بنابراین new-post اشتباها به جای id گرفته می شود و سرور تمرینی JSONPlaceholder نیز هیچ پستی با آیدی new-post ندارد بنابراین در کنسول به ما خطا نشان می دهد. بنابراین یادتان باشد که ترتیب مهم است.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.