نمایش محتوای پست ها در FullPost

22 بهمن 1399
نمایش محتوای پست ها در FullPost

نمایش محتوای پست ها در FullPost

در قسمت قبل توانستیم پست ها را قابل انتخاب کنیم اما کار زیادی با Axios یا درخواست های HTTP نداشتیم و بیشتر آن کار با کدهای React بود. اگر یادتان باشد محتویات فایل FullPost.js بدین شکل بود:

    render() {
        let post = <p style={{ textAlign: 'center' }}>Please select a Post!</p>;

        if (this.props.id) {
            post = (
                <div className="FullPost">
                    <h1>Title</h1>
                    <p>Content</p>
                    <div className="Edit">
                        <button className="Delete">Delete</button>
                    </div>
                </div>

            );
        }

        return post;
    }

بنابراین باید کاری کنیم تا به محض دریافت id جدید، یک درخواست HTTP ارسال کرده و محتوای مورد نظرمان را تحویل بگیریم. به نظر شما از چه lifecycle hook ای باید استفاده کنیم؟ اگر یادتان باشد خلاصه ی lifecycle hook های مربوط به «چرخه ی ویرایش» را برایتان توضیح داده بودیم:

Lifecycle hooks مربوط به چرخه ی ویرایش
Lifecycle hooks مربوط به چرخه ی ویرایش

چرا «چرخه ی ویرایش»؟ به دلیل اینکه کامپوننت از همان اول هم ساخته شده است! درست است که هنوز اطلاعات مورد نظر ما را ندارد اما از نظر وجودی، کامپوننت ساخته شده و وجود دارد بنابراین از اینجا به بعد هر کاری انجام دهیم ویرایش به حساب می آید. همانطور که در تصویر بالا می بینید componentDidUpdate یک hook خوب برای ایجاد side-effect محسوب می شود بنابراین می توانیم از آن استفاده کنیم اما خطر بزرگی در استفاده از این hook وجود دارد! اگر درون componentDidUpdate مقدار state را دستکاری/ویرایش کنیم باعث render شدن دوباره می شویم و بدین شکل وارد یک حلقه ی نامحدود (infinite loop) میشویم! بنابراین هنگام استفاده از آن باید بسیار مراقب باشیم.

برای شروع وارد فایل FullPost.js میشویم و Axios را در آن import می کنیم:

import axios from 'axios';

حالا می توانیم وارد کلاس شویم و در آن componentDidUpdate را اضافه کنیم:

class FullPost extends Component {

    componentDidUpdate () {
        axios.get()
    }

    render() {
// بقیه ی کدها //

به نظر شما درون get چه url ای باید قرار بگیرد؟ در این قسمت url باید یک پست را هدف بگیرد، همان پستی که id اش را داریم! اگر به https://jsonplaceholder.typicode.com/ بروید یک آدرس را برای این منظور می بینید:

آدرس های مختلف در سایت JSONPlaceholder (آدرس مورد نظر ما دومی است که id پست را همراه خود دارد)
آدرس های مختلف در سایت JSONPlaceholder (آدرس مورد نظر ما دومی است که id پست را همراه خود دارد)

منظور من آدرس https://jsonplaceholder.typicode.com/posts/1 است که یک نمونه است. id پست ها در این ساختار url در انتهای آدرس قرار می گیرد. بنابراین این url آیدی پست اول را به ما می دهد. به همین صورت می توانیم id مورد نظرمان را از url دریافت کنیم (البته آن را درون get می گذاریم تا با توجه به پست انتخاب شده، بروزرسانی شود):

    componentDidUpdate() {
        axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id);
    }

بدین شکل id هر پست انتخاب شده توسط کاربر به انتهای url می چسبد و پست آن را دریافت می کند. البته این کد مشکلی دارد؛ اگر id برابر null باشد باز هم درخواست ارسال می شود بنابراین بهتر است با یک شرط if آن را مهار کنیم:

    componentDidUpdate() {
        if (this.props.id) {
            axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id);
        }
    }

اگر مقدار id برابر با null باشد، this.props.id هیچ گاه true نشده و درخواستی نیز ارسال نخواهد شد. با اجرای این کد درخواست ما ارسال می شود و داده به سمت برنامه ی ما پاس داده می شود اما هیچ کاری برای مدیریت یا نگهداری از این داده انجام نداده ایم! برای این کار از متد then استفاده می کنیم:

componentDidUpdate() {
    if (this.props.id) {
        axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
            console.log(Response);
        });
    }
}

بدین صورت به محض کلیک روی یکی از عناوین پست ها، باید شیء حاوی آن را در console مرورگر ببینیم. اگر به مرورگر رفته و آن را تست کنیم:

شیء برگردانده شده از JSONPlaceholder برای یک پست با آیدی خاص (در این مورد id برابر 1 بوده است)
شیء برگردانده شده از JSONPlaceholder برای یک پست با آیدی خاص (در این مورد id برابر 1 بوده است)

این همان شیء پست مورد نظر ما است که به ما برگردانده می شود. بنابراین برنامه به صورت صحیح کار می کند و حالا وقت آن است که مطالب را درون مرورگر قرار دهیم نه درون console. برای این کار درون فایل FullPost.js باید state را تعریف کنیم:

    state = {
        loadedPost: null
    }

مقدار پست را در حالت پیش فرض روی null گذاشته ایم. حالا در قسمت JSX همین فایل می گوییم:

if (this.props.id) {
    post = (
        <div className="FullPost">
            <h1>{this.state.loadedPost.title}</h1>
            <p>{this.state.loadedPost.body}</p>
            <div className="Edit">
                <button className="Delete">Delete</button>
            </div>
        </div>

    );
}

احتمالا از خودتان بپرسید که چرا loadedPost.body؟ دلیلش این است که سرور تمرینی ما چنین شیء ای را پاس می دهد:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

این شیء همانطور که مشخص است یک شیء JSON است بنابراین باید بر اساس این شیء کدنویسی کنیم تا مقادیرش را دریافت کنیم. حالا به componentDidUpdate برمی گردیم و به جای log کردن کنسول state را تغییر می دهیم:

    componentDidUpdate() {
        if (this.props.id) {
            axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
                // console.log(Response);
                this.setState({ loadedPost: Response.data });
            });
        }
    }

اگر در این مرحله کدها را ذخیره کرده و به مرورگر برویم، با کلیک روی هر یک از پست ها با خطای زیر روبرو می شویم:

Uncaught TypeError: Cannot read property 'title' of null at FullPost.render

این خطا به ما می گوید که نمی توانم مقدار title را بخوانم چرا که null است. سعی کنید با چند دقیقه فکر خودتان متوجه اشتباه بشوید...

پاسخ این مشکل کمی مخفی است و شاید نتوانید به این سادگی ها آن را پیدا کنید. به کد زیر نگاه کنید:

if (this.props.id) {
    post = (
        <div className="FullPost">
            <h1>{this.state.loadedPost.title}</h1>
            <p>{this.state.loadedPost.body}</p>
            <div className="Edit">
                <button className="Delete">Delete</button>
            </div>
        </div>

    );
}

در این کد (در قسمت شرط if) به محض اینکه مقدار this.props.id برابر true شود، وارد شرط می شویم و سعی می کنیم به this.state.loadedPost.title دسترسی پیدا کنیم. تکرار میکنم که این کار را به محض دریافت id (از شرط this.props.id) انجام می دهیم. اگر دقت کرده باشید ما مقدار id را بسیار قبل تر از آنکه loadedPost آماده باشد دریافت می کنیم. دریافت داده های جاوااسکریپت نامتقارن است! باید دوباره به کد componentDidUpdate نگاه کنید:

    componentDidUpdate() {
        if (this.props.id) {
            axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
                // console.log(Response);
                this.setState({ loadedPost: Response.data });
            });
        }
    }

ما ابتدا در قسمت شرط if مقدار id را دریافت می کنیم و سپس در چند خط بعد مقدار loadedPost را با setState تنظیم می نماییم. در واقع زمانی که ما DOM را re-render می کنیم، هنوز loadedPost ای غیر از null وجود ندارد... این مشکل ما است!

برای حل این مشکل می توان گفت:

render() {
    let post = <p style={{ textAlign: 'center' }}>Please select a Post!</p>;

    if (this.props.id) {
        post = <p style={{ textAlign: 'center' }}>Loading...!</p>;
    }

    if (this.state.loadedPost) {
        post = (
            <div className="FullPost">
                <h1>{this.state.loadedPost.title}</h1>
                <p>{this.state.loadedPost.body}</p>
                <div className="Edit">
                    <button className="Delete">Delete</button>
                </div>
            </div>

        );
    }

    return post;
}

در واقع در این کد می گوییم زمانی که کاربر روی یکی از عناوین پست ها کلیک کرد (و طبعا ما id را دریافت کردیم) یک پاراگراف به کاربر نمایش بده که می گوید Loading (یعنی در حال بارگذاری). سپس در شرط بعدی می گوییم post را به مقدار مناسب تغییر بده البته در این شرط به جای چک کردن id مقدار loadedPost را چک می کنیم. به همین راحتی!

حالا به مرورگر بروید و با کلیک روی عناوین مختلف، متن های پست ها را مشاهده کنید (در حد چند لحظه می توانید عبارت Loading را نیز مشاهده کنید). البته این کد ما مشکل بسیار بزرگی دارد! اگر روی یکی از عناوین پست ها کلیک کنید، پست برایتان به نمایش در می آید و مشکلی نیست اما اگر در قسمت dev tools به سربرگ network بروید متوجه می شوید که برنامه ی ما به صورت بی وقفه در حال ارسال درخواست به سرور است!

تعداد درخواست های ارسالی در هر ثانیه افزایش می یابد و در چند ثانیه به 400 مورد رسیده است!
تعداد درخواست های ارسالی در هر ثانیه افزایش می یابد و در چند ثانیه به 400 مورد رسیده است!

همانطور که می بینید در طی چند ثانیه 430 درخواست به سرور ارسال شده است!!!! مشکل کجاست؟

مشکل آنجاست که ما state را از درون componentDidUpdate ویرایش کرده ایم:

    componentDidUpdate() {
        if (this.props.id) {
            axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
                // console.log(Response);
                this.setState({ loadedPost: Response.data });
            });
        }
    }

زمانی که setState را صدا بزنیم، کامپوننت هایمان بروزرسانی می شوند و به همین خاطر componentDidUpdate اجرا خواهد شد و زمانی که componentDidUpdate اجرا شود، دستور setState درونش تغییر می کند و بدین شکل وارد یک حلقه ی نامحدود و بی پایان می شویم!

برای حل این مشکل باید چک کنیم که درخواست ما فقط زمانی ارسال شود که روی یک پست جدید کلیک کرده باشیم. بنابراین:

componentDidUpdate() {
    if (this.props.id) {
        if (this.state.loadedPost && this.state.loadedPost.id !== this.props.id) {
            axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
                // console.log(Response);
                this.setState({ loadedPost: Response.data });
            });
        }

    }
}

در این شرط گفته ایم اگر this.state.loadedPost وجود داشته باشد (True باشد و مقدار اولیه ی null را نگرفته باشد) و همچنین مقدار loadedPost.id برابر با مقدار id پاس داده شده از props نباشد (یعنی روی پست قبلی کلیک نشده باشد بلکه روی یک پست جدید کلیک کرده باشیم) آنگاه درخواست را ارسال کن. این کد به نظر درست می آید اما هنوز مشکلی دارد! شرط ما زمانی برقرار است که حداقل یک پست را داشته باشیم درست است؟ در حالت پیش فرض و در هنگام رفرش کردن صفحه مقدار loadedPost را روی null گذاشته ایم بنابراین حتی اگر کلیک نیز بکنیم، مشکل رفع نمی شود و برنامه در همان حالت اول باقی میماند. برای حل این مشکل می توانیم کمی شرط خود را پیشرفته تر کنیم:

    componentDidUpdate() {
        if (this.props.id) {
            if (!this.state.loadedPost || (this.state.loadedPost && this.state.loadedPost.id !== this.props.id)) {
                axios.get('https://jsonplaceholder.typicode.com/posts/' + this.props.id).then(Response => {
                    // console.log(Response);
                    this.setState({ loadedPost: Response.data });
                });
            }

        }
    }

یعنی گفته ایم اگر loadedPost وجود نداشته باشد یا اینکه اگر وجود داشته باشد باید id متفاوتی از مقدار props.id داشته باشد. امیدوارم به خوبی مفاهیم این جلسه را درک کرده باشید.

دانلود کدهای این فصل تا این جلسه

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.