حل یک مشکل جدی در قسمت مدیریت خطا + بهینه سازی کدها (پایان فصل 7)

22 بهمن 1399
حل یک مشکل جدی در قسمت مدیریت خطا + بهینه سازی کد ها (پایان فصل 7)

حل یک مشکل جدی در قسمت مدیریت خطا

در جلسه ی قبل کاری کردیم که محتوای اولیه ی همبرگر، به جای state، مستقیما از Firebase دریافت شود اما اگر خوب به کدها دقت کنید به یک مشکل بزرگ برمی خوریم. فرض کنید من url درخواست get را برای محتویات اولیه خراب کنم. مثلا json. آخر url را از آن حذف کنم:

    componentDidMount() {
        axios.get('https://react-my-burger-4229e.firebaseio.com/ingredients')
            .then(response => {
                this.setState({ ingredients: response.data });
            });
    }

همانطور که می دانید firebase از شما می خواهد که حتما در آخر url ها مقدار json. را داشته باشید. این کد را ذخیره کرده و به مرورگر بروید. بله همانطور که مشاهده می کنید به جای دریافت modal خطا، spinner ای را داریم که بدون توقف نمایش داده می شود. به نظر شما مشکل چیست؟

ما error handler سراسری خود را در فایل withErrorHandler.js تعریف کردیم و interceptor ها را درون componentDidMount قرار داده ایم:

componentDidMount() {

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

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

آیا چرخه ی زندگی lifecycle hooks را به یاد دارید؟

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

همانطور که می بینید componentDidMount بعد از render شدن تمام کامپوننت های فرزند (child component) صدا زده می شود. یعنی زمانی که componentDidMount موجود در کامپوننت های فرزند تکمیل شود، آنگاه componentDidMount پدر نیز صدا زده خواهد شد.

حالا اگر به فایل withErrorHandler برگردید:

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

در اینجا ما یک عنصر WrappedComponent داریم که هر عنصری است که درون withErrorHandler قرار بگیرد. در برنامه ی ما این عنصر، همان کامپوننت BurgerBuilder است:

export default withErrorHandler(BurgerBuilder, axios);

آیا متوجه مشکل شدید؟ componentDidMount ای که درون withErrorHandler است تنها زمانی صدا زده می شود که componentDidMount درون BurgerBuilder صدا زده شده و تکمیل شود. از آنجایی که ما درون componentDidMount فایل BurgerBuilder به سرور Firebase درخواست ارسال می کنیم، هیچ interceptor ای را تکمیل نمی کنیم!

برای حل این مشکل دو راه وجود دارد:

  • استفاده از componentWillMount به جای componentDidMount
  • استفاده از constructor کلاس

من از componentWillMount استفاده می کنم اما همانطور که می دانید componentWillMount در نسخه های بعدی react منسوخ خواهد شد. در این صورت شما می توانید با روش هایی که برایتان توضیح دادیم، interceptor ها را درون constructor تعریف کنید. بنابراین componentDidMount درون فایل withErrorHandler را به componentWillMount تغییر دهید:

componentWillMount() {

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

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

اگر الان به مرورگر بروید ابتدا modal خطا را می گیریم و سپس این modal بسته شده و یک خطای دیگر را مشاهده می کنیم که میگوید نتوانسته ایم setState را اجرا کنیم. ما می توانیم به اضافه کردن یک متد catch در درخواست get خود مشکل را برطرف کنیم. به فایل BurgerBuilde.js رفته و در قسمت componentDidMount متد catch را نیز اضافه می کنیم:

componentDidMount() {
    axios.get('https://react-my-burger-4229e.firebaseio.com/ingredients')
        .then(response => {
            this.setState({ ingredients: response.data });
        })
        .catch(error => { });
}

اگر با error دریافت شده (مثل کد بالا) هیچ کاری انجام ندهیم، پنجره ی modal برای خطا ثابت می ماند اما اگر کاربر روی backdrop کلیک کند، modal بسته شده و باز هم spinner برای خودش میچرخد بنابراین بهتر است با این خطای خاص کار خاصی انجام دهیم. من به state یک خصوصیت جدید به نام error اضافه می کنم:

    state = {
        ingredients: null,
        totalPrice: 4,
        purchasable: false,
        purchasing: false,
        loading: false,
        error: false
    }

حالا اگر خطایی دریافت کردیم، مقدار state.error را روی true می گذاریم:

componentDidMount() {
    axios.get('https://react-my-burger-4229e.firebaseio.com/ingredients')
        .then(response => {
            this.setState({ ingredients: response.data });
        })
        .catch(error => {
            this.setState({ error: true });
        });
}

حالا به پایین تر می رویم تا به متغیر burger برسیم و آن را بدین شکل تغییر می دهیم:

let burger = this.state.error ? <p>Ingredients can't be loaded!</p> : <Spinner />;

بدین صورت اگر state.error ما برابر با true باشد به جای همبرگر، یک پاراگراف می بینیم که میگوید محتویات قابل بارگذاری نیست! حالا اگر به مرورگر برویم چنین صفحه ای را می بینیم:

نمایش Modal خطا به همراه پاراگراف خطا به جای spinner دائمی
نمایش Modal خطا به همراه پاراگراف خطا به جای spinner دائمی

حالا حتی اگر کاربر روی backdrop کلیک کند و modal خطا بسته شود، باز هم روی spinner نمیمانیم بلکه پیام Ingredients can't be loaded نمایش داده می شود و کاربر متوجه خواهد شد که برنامه دچار اختلال است.

بهینه سازی برنامه

برنامه ی ما برنامه ی بسیار خوبی است اما مشکلی دارد که بعد ها در دوره با آن مواجه خواهیم شد! همانطور که می دانید ما یک error handler سراسری را در فایل withErrorHandler.js ایجاد کردیم که تابعی به شکل زیر است:

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

        state = {
            error: null
        }

        componentWillMount() {

            // بقیه ی کدها

این error handler سراسری مخصوص یک کامپوننت نیست و آن را طوری نوشته ایم که بتوان از آن در چندین کامپوننت استفاده کرد. مشکل آنجاست که با هر بار استفاده از withErrorHandler در کامپوننت های مختلف، componentWillMount را دوباره صدا میزنیم چرا که با هر بار استفاده از withErrorHandler کلاس موجود در تابع withErrorHandler (کد بالا) دوباره ساخته می شود و componentWillMount نیز حاوی interceptor های ما است بنابراین interceptor های ما دوباره ساخته می شوند. ما تمام این interceptor ها را به یک instance واحد از axios میچسبانیم. شاید در حال حاضر مشکلی نداشته باشیم اما بعدا زمانی که routing (صفحه بندی) را به پروژه ی خود اضافه کنیم به مشکل برمی خوریم. زمانی که صفحات مختلفی داشته باشیم باید آن ها را در withErrorHandler قرار دهیم که باعث می شود به تعداد interceptor های ما اضافه شود. مشکل اصلی آنجاست که اگر با یک صفحه کار داشته باشیم، interceptor های صفحات دیگر که از قبل ساخته شده اند، همانطور در حافظه ی برنامه ی ما باقی میمانند و به کدهای جدید واکنش نشان می دهند! در بهترین حالت حافظه ی برنامه ی ما (memory) به شدت اشغال می شود و در بدترین حالت نیز برنامه ی ما با خطا مواجه شده یا state به صورت ناخواسته تغییر می کند و...

راه حل آن است که هنگام unmount شدن کامپوننت مورد نظر، interceptor ها نیز حذف شوند. برای این کار از یک lifecycle hook به نام componentWillUnmount استفاده می کنیم. به فایل withErrorHandler بروید و کد زیر را پس از componentDidMount به آن اضافه کنید:

componentWillUnmount () {
            
}

برای آنکه بتوانیم درون این متد interceptor ها را حذف کنیم باید یک ارجاع به interceptor ها را درون یک خصوصیت (property) کلاسشان ذخیره کنیم. state یک خصوصیت (property) محسوب می شود و ما هم می توانیم یک خصوصیت جدید ایجاد کنیم. به طور مثال:

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

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

شما می توانید به جای نام های reqInterceptor یا resInterceptor هر نام دیگری را برای property های خود انتخاب کنید. با این کار ما interceptor ها را در خصوصیاتی به نام های reqInterceptor یا resInterceptor قرار داده ایم و می توانیم آن ها را در componentWillUnmount حذف کنیم:

componentWillUnmount() {
    axios.interceptors.request.eject(this.reqInterceptor);
    axios.interceptors.response.eject(this.resInterceptor);
}

دستور eject مخصوص axios است و یک interceptor را حذف می کند اما نیاز دارد که ارجاعی به آن بدهیم تا بداند کدام interceptor را حذف کند. به همین دلیل بود که ما interceptor ها را درون خصوصیت هایشان قرار دادیم.

حالا برنامه ی ما بهینه سازی شده است! یادتان نرود که در فایل BurgerBuilder.js آدرس محتویات را به حالت قبل و صحیح خود برگردانید:

axios.get('https://react-my-burger-4229e.firebaseio.com/ingredients.json')

دانلود کدهای کامل این فصل

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

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