مدیریت خطا و اعلام آن به کاربر

22 بهمن 1399
مدیریت خطا و اعلام آن به کاربر

مدیریت خطا و اعلام آن به کاربر

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

برای مدیریت خطاها می خواهیم یک Error Handler سراسری تعریف کنیم تا فارغ از اینکه خطا در کدام کامپوننت رخ داده است، آن ها را مدیریت کرده و پیامی به کاربر نمایش دهد. این Error Handler به صورت یک Modal خواهد بود و از آنجایی که سراسری است به یک کامپوننت بالاتر از BurgerBuilder اضافه خواهد شد.

برای شروع وارد پوشه ی hoc شوید و پوشه ی جدیدی به نام withErrorHandler ایجاد کنید که حاوی فایلی به همین نام باشد (با پسوند js). محتویات این فایل برای شروع به شکل زیر است:

import React from 'react';

const withErrorHandler = (WrappedComponent) => {
    return (props) => {
        return (
            <WrappedComponent {...props} />
        );
    }
}

export default withErrorHandler;

این کامپوننت یک کامپوننت کاربردی است که WrappedComponent را می گیرد (همان عنصری که درون این hoc خواهد بود) سپس یک تابع دیگر برمی گرداند که خود آن تابع کد JSX بالا را برمی گرداند؛ یعنی WrappedComponent را به همراه props هایش برمی گرداند. توجه کنید که از اپراتور spread (علامت سه نقطه) استفاده کرده ایم تا prop ها را به هم نچسبانیم و عینا هر prop ای که در WrappedComponent بوده است برگردانده شود.

حالا Modal و Aux را نیز وارد می کنیم:

import Modal from '../../components/UI/Modal/Modal';
import Aux from '../Auxx/Auxx';

و آن ها را در قسمت JSX اضافه می کنیم:

return (
    <Aux>
        <Modal>
            Something didn't work!
        </Modal>
        <WrappedComponent {...props} />
    </Aux>
);

در جلسات قبل با کامپوننت های Aux و Modal آشنا شدیم و می دانیم کارشان چیست. من می خواهم علاوه بر WrappedComponent یک Modal نیز نمایش بدهم که در آن به کاربر بگویم چه اتفاقی افتاده و چرا برنامه خراب شده است. در حال حاضر فقط متن Something didn’t work را نوشته ایم که یعنی «چیزی خراب شده است».

حالا به فایل BurgerBuilder بروید و withErrorHandler را import کنید:

import withErrorHandler from '../../hoc/withErrorHandler/withErrorHandler';

و سپس در قسمت export (قسمت انتهایی فایل BurgerBuilder.js) آن را درون withErrorHandler قرار می دهیم:

export default withErrorHandler(BurgerBuilder);

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

برای تست این کدها به فایل withErrorHandler.js بروید و به کد Modal مقدار show را اضافه کنید:

        return (
            <Aux>
                <Modal show>
                    Something didn't work!
                </Modal>
                <WrappedComponent {...props} />
            </Aux>
        );

اگر یادتان باشد این خصوصیت را در هنگام تعریف Modal ایجاد کرده بودیم. حالا اگر به مرورگر برویم، همیشه مقدار something didn’t work را مشاهده خواهیم کرد:

نمایش پیام something didn't work
نمایش پیام something didn't work

بنابراین کدهای ما تا اینجا به طور صحیح کار می کنند و از این نظر مشکلی نداریم اما نمی خواهیم modal ما همیشه نمایش داده شود بلکه باید زمانی نمایش داده شود که با خطایی روبرو می شویم. این خطا هم مشخصا از سمت WrappedComponent می آید و در برنامه ی ما همان خطای احتمالی در ارتباط با سرور Firebase است بنابراین جدا از WrappedComponent باید axios را نیز داشته باشیم تا بتوانیم خطاهایش را بررسی کنیم:

const withErrorHandler = (WrappedComponent, axios) => {
    return (props) => {
// بقیه ی کدها //

در اینجا می خواهیم از interceptor های Axios در componentDidMount استفاده کنیم، بنابراین بهتر است این کامپوننت را تبدیل به یک کامپوننت کلاس-محور کنیم (البته می توانیم از hook ای به نام useEffect نیز استفاده کنیم اما فعلا آن را به یک کامپوننت کلاس-محور تبدیل می کنیم). بنابراین ابتدا component را به دسترو import اضافه می کنیم:

import React, { Component } from 'react';

سپس:

const withErrorHandler = (WrappedComponent, axios) => {
    return class extends Component {

        render() {
            return (
                <Aux>
                    <Modal show>
                        Something didn't work!
                        </Modal>
                    <WrappedComponent {...this.props} />
                </Aux >
            )
        }
    }
}

همانطور که می بینید تابع دوم را تبدیل به یک کامپوننت کلاس-محور کرده ایم؛ البته همانطور که می بینید یک کلاس ناشناس (anonymous) است چرا که نام ندارد. حالا می توانیم از componentDidMount استفاده کنیم اما ابتدا یک state برای این کلاس تعریف می کنیم:

state = {
    error: null
}

سپس componentDidMount را می نویسیم:

componentDidMount() {

    axios.interceptors.request.use(req => {
        this.setState({ error: null });
    });

    axios.interceptors.response.use(null, error => {
        this.setState({ error: error });
    });
}

ما در اینجا دو interceptor تعریف کرده ایم (با نحوه ی تعریف آن ها در جلسات قبل آشنا شدیم) که مورد اول برای درخواست ها و مورد دوم برای پاسخ دریافتی است. برای درخواست ها نمی خواهیم هیچ خطایی نمایش داده شود چرا که هنوز در حال ارسال درخواست هستیم و اگر هنوز درخواستی ارسال نشده چطور می توانیم خطایی داشته باشیم؟ interceptor دوم نیز مخصوص پاسخ ها است که از سمت Firebase برایمان ارسال خواهد شد. اگر یادتان باشد پارامتر اول use برای پاسخ ها response بود که به صورت خودکار به تابع پاس داده می شود. ما با این پارامتر هیچ کاری نداریم، بلکه فقط دنبال خطاها هستیم که پارامتر دوم هستند؛ برای آن ها نیز مقدار state.error را روی error برگردانده شده از سمت firebase میگذاریم. در واقع axios درخواست را به Firebase ارسال می کند و Firebase نیز پاسخ را برمی گرداند که ما به صورت شیء error به آن دسترسی داریم. توجه داشته باشید که این شیء ارسال شده توسط Firebase یک پیام نیز در خود دارد که ما از آن استفاده خواهیم کرد.

به قسمت JSX بروید و کدها را به شکل زیر تغییر دهید:

render() {
    return (
        <Aux>
            <Modal show={this.state.error}>
                {this.state.error.message}
            </Modal>
            <WrappedComponent {...this.props} />
        </Aux >
    )
}

دو چیز را در کد بالا تغییر داده ایم: مقدار show را روی error برگردانده شده از سمت Firebase گذاشته ایم تا اگر true بود خطا نمایش داده شده و اگر false بود هیچ چیزی نمایش داده نشود. همچنین به جای پیام ساده ی something didn’t work از همان شیء error و خصوصیت message آن استفاده کرده ایم تا پیام خطای اصلی را به کاربر نمایش دهیم.

تا اینجای کار همه چیز خوب پیش رفته است اما یک مشکل داریم: اگر درخواست ما با خطایی مواجه شود و Modal برای کاربر باز شود، دیگر هیچ راهی برای بستن آن نیست و کاربر باید صفحه را refresh کند. بنابراین از prop ای به نام modalClosed استفاده می کنیم که در هنگام تعریف کامپوننت Modal برایش تعریف کرده بودیم. برای این کار ابتدا یک متد تعریف می کنیم:

errorConfirmedHandler = () => {
    this.setState({ error: null });
}

سپس آن را در modalClosed برای Modal استفاده می کنیم:

return (
    <Aux>
        <Modal show={this.state.error} modalClosed={this.errorConfirmedHandler}>
            {this.state.error.message}
        </Modal>
        <WrappedComponent {...this.props} />
    </Aux >
)

حالا مشکل دیگری داریم! در کد بالا قسمت this.state.error.message باعث خطا خواهد شد چرا که Modal همیشه حضور دارد حتی اگر نمایش داده نشود (نمایش داده شدن تفاوت زیادی با عدم وجود دارد) بنابراین حتی اگر خطایی نباشد و Modal نمایش داده نشود، this.state.error.message سعی می کند که پیام خطای را دریافت کند که اصلا وجود ندارد و خودش باعث خطا می شود! برای رفع این مشکل میگوییم:

return (
    <Aux>
        <Modal show={this.state.error} modalClosed={this.errorConfirmedHandler}>
            {this.state.error ? this.state.error.message : null}
        </Modal>
        <WrappedComponent {...this.props} />
    </Aux >
)

بدین شکل اگر state.error برابر با false باشد، دنبال پیام خطا هم نمیگردیم. در آخر به فایل BurgerBuilder.js بروید و Axios را به withErrorHandler پاس بدهید:

export default withErrorHandler(BurgerBuilder, axios);

حالا اگر به مرورگر برویم با خطا مواجه می شویم! چرا؟ به دلیل اینکه اگر یادتان باشد در interceptor ها باید درخواست و پاسخ را return می کردیم در غیر این صورت درخواست متوقف می شد (اگر یادتان نمی آید به قسمت مربوط به interceptor ها مراجعه کنید). بنابراین interceptor هایمان را بدین شکل ویرایش می کنیم:

componentDidMount() {

    axios.interceptors.request.use(req => {
        this.setState({ error: null });
        return req;
    });

    axios.interceptors.response.use(res => res, error => {
        this.setState({ error: error });
    });
}

interceptor اول req (درخواست) را return می کند و interceptor دوم نیز res (پاسخ) را return می کند (حالت res => res خلاصه ترین روش برای return کردن است).

فعلا برنامه ی ما به صورت صحیح کار می کند بنابراین برای تست خطاها باید به صورت عمدی جایی را خراب کنیم. برای این کار به فایل BurgerBuilder.js بروید و json. در آخر درخواست را حذف کنید. یعنی کد زیر را:

 axios.post('/orders.json', order)

به این کد تغییر دهید:

axios.post('/orders', order)

حالا با ثبت سفارش با خطای شبکه روبرو می شویم:

دریافت خطای شبکه و نمایش آن به کاربر
دریافت خطای شبکه و نمایش آن به کاربر

این متن از سمت Firebase به ما ارسال شده است. حالا باید با کلیک روی backdrop (قسمت تیره پشت Modal) پیام خطا بسته شود که این اتفاق نیز می افتد. حالا میتوانید آدرس درخواست post (همان orders.json) را به حالت قبل برگردانید.

حالا یک error handler سراسری داریم که می توانیم در هر جای برنامه از آن استفاده کنیم.

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

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