Redux پیشرفته در پروژه همبرگرساز – کد نامتقارن

Advanced Redux in Burger Builder Project - Asymmetric Code

22 بهمن 1399
redux پیشرفته در پروژه ی همبرگر ساز – کد نامتقارن

در جلسه قبل پروژه خود را تا حد قابل قبولی با مفاهیم پیشرفته redux جلو بردیم و حالا نوبت یکی از اصلی ترین مباحث یعنی اجرای نامتقارن کدها در جاوا اسکریپت است. یکی از ساده ترین قسمت های برنامه ما که در حال حاضر می تواند کدها را به صورت نامتقارن اجرا کند در فایل BurgerBuilder.js (در پوشه containers) است. اگر یادتان باشد ما محتویات همبرگر اولیه را از componentDidMount دریافت می کردیم:

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

البته همانطور که می بینید زمانی که از redux استفاده کردیم کدهای این قسمت را کامنت کردیم اما حالا دوباره می توانیم از این قسمت استفاده کنیم.

به صورت کلی دو راه وجود دارد:
راه اول این است که از کد بالا استفاده کرده و به جای this.setState کردن آن، نوعی action را dispatch کنید تا محتویات همبرگر را در store ما به روز رسانی کند. در چنین حالتی کدهای نامتقارن خود را در همین کامپوننت پیاده سازی کرده ایم و دیگر نیازی به action creator ها نیازی نخواهیم داشت. چرا؟ به دلیل اینکه در همین کامپوننت، action خود را ارسال کرده ایم و نیازی به action creator ها برای ارسال آن ها نیست. انتخاب این روش هیچ مشکلی ندارد اما هدف اصلی action creator ها این بود که بتوانید کدهای نامتقارن خود را درون redux قرار دهید بنابراین من برای اهداف آموزشی از روش دوم استفاده می کنم.
روش دوم همان روشی است که با استفاده از middleware ها و redux-thunk در پروژه شمارنده پیاده سازی کرده بودیم.

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

npm install --save redux-thunk

حالا به فایل index.js بروید (مستقیما در پوشه src) و دستور import خود را به شکل زیر بنویسید:

import thunk from 'redux-thunk';

سپس باید middleware ها را نیز از redux اضافه کنیم بنابراین دستور import آن را نیز به شکل زیر ویرایش می کنیم:

import { createStore, applyMiddleware, compose } from 'redux';

تمام این موارد را در هنگام توسعه پروژه شمارنده انجام داده بودیم. مثل همیشه به صفحه گیت هاب redux-devtools می رویم تا طبق دستور العمل بخش advanced عمل کنیم چرا که حالا می خواهیم middleware ها را نیز به پروژه اضافه کنیم. طبق دستور العمل بخش advanced این ابزار:

  import { createStore, applyMiddleware, compose } from 'redux';

+ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+ const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
- const store = createStore(reducer, /* preloadedState, */ compose(
    applyMiddleware(...middleware)
  ));

من طبق همین دستورالعمل کدها را کپی می کنم. ابتدا composeEnhancers:

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

سپس باید آن را به CreateStore داده و applyMiddleware را پیاده سازی کنیم:

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(burgerBuilderReducer, composeEnhancers(
    applyMiddleware(thunk)
));

حالا به مرورگر بروید. باید بتوانید صفحه را refresh کرده و همچنین redux devtools را باز کنید. اگر اینطور است یعنی نوبت به مرحله بعد و پیاده سازی کدهای نامتقارن است. برای دریافت محتویات به صورت نامتقارن باید به پوشه actions و سپس burgerBuilder.js برویم. در آنجا یک actionCreator جدید می سازم:

export const initIngredients = () => {

};

در اکثر اوقات درون این actionCreator یک action را برمی گردانیم اما در این حالت خاص قضیه متفاوت است. در اینجا می خواهیم اطلاعات را از یک سرور بگیریم و از redux-thunk استفاده می کنیم بنابراین تابع dispatch را دریافت می کنیم:

export const initIngredients = () => {
    return dispatch => {
        
    }
};

حالا می توانیم درون این تابع کدهای نامتقارن خود را بنویسیم و در آخر action خود را dispatch کنیم. اگر از پروژه شمارنده یادتان باشد در این قسمت نیاز به یک actionCreator دیگر داشتیم تا یک action  را برگرداند:

export const setIngredients = ( ingredients ) => {
    return {
        type: actionTypes.SET_INGREDIENTS,
        ingredients: ingredients
    };
};

export const initIngredients = () => {
    return dispatch => {

    }
};

اگر دقت کنید ما actionType ای به نام SET_ INGREDIENTS نداشتیم بنابراین به فایل actionTypes.js بروید و آن را تعریف کنید:

export const ADD_INGREDIENT = 'ADD_INGREDIENT';
export const REMOVE_INGREDIENT = 'REMOVE_INGREDIENT';
export const SET_INGREDIENTS = 'SET_INGREDIENTS';

حالا دوباره به کد زیر نگاه کنید:

export const setIngredients = ( ingredients ) => {
    return {
        type: actionTypes.SET_INGREDIENTS,
        ingredients: ingredients
    };
};

ما ingredients یا محتویات همبرگر را به صورت آرگومان دریافت کرده ایم و آن را به همراه actionType مورد نظر ارسال کرده ایم. این actionCreator از نوع متقارن است بنابراین باید از آن در initIngredients استفاده کنیم که هنوز نیمه کاره است. در واقع زمانی که اجرای کدهای نامتقارن درون initIngredients برای دریافت محتویات از سرور firebase تمام شد، تابع setIngredients وارد عمل شده و action مورد نظر را dispatch می کند.

حالا به فایل BurgerBuilder.js (درون پوشه containers) رفته و کدهای Axios مربوط به دریافت محتویات از Firebase را cut کرده و مثل من درون initIngredients قرار دهید:

export const setIngredients = ( ingredients ) => {
    return {
        type: actionTypes.SET_INGREDIENTS,
        ingredients: ingredients
    };
};

export const initIngredients = () => {
    return dispatch => {
        axios.get( 'https://react-my-burger.firebaseio.com/ingredients.json' )
            .then( response => {
                this.setState( { ingredients: response.data } );
            } )
            .catch( error => {
                this.setState( { error: true } );
            } );
    };
};

در این مرحله من خطایی دریافت می کنم که به axios دسترسی نداریم بنابراین در ابتدای همین فایل (burgerBuilder.js درون پوشه actions) یک دستور import برای axios قرار می دهم:

import axios from '../../axios-orders';

حالا به کد اصلی یعنی initIngredients برمی گردیم. ما باید به جای دستور setState از actionCreator های خودمان استفاده کنیم بنابراین می توان گفت:

export const initIngredients = () => {
    return dispatch => {
        axios.get( 'https://react-my-burger.firebaseio.com/ingredients.json' )
            .then( response => {
               dispatch(setIngredients(response.data));
            } )
            .catch( error => {

            } );
    };
};

به همین سادگی، هنگامی که پاسخ از سمت سرور به ما برسد می توانیم آن را به setIngredients داده و action خود را به سمت reducer ارسال یا همان dispatch کنیم. سوال دیگر که پیش می آید، قسمت خالی در کد بالا برای error (خطا) است. اگر کد ما به هر دلیلی با موفقیت اجرا نشود (مثلا سرور خراب باشد) آنگاه چه اتفاقی می افتد؟

ابتدا به فایل BurgerBuilder.js (درون پوشه containers) رفته و به State نگاهی بیندازید:

    state = {
        purchasing: false,
        loading: false,
        error: false
    }

قبل از استفاده از redux هر دو مورد error و loading را در همین کامپوننت مدیریت می کردیم اما حالا می خواهیم آن را در redux مدیریت کنیم بنابراین آن دو را از State قبلی حذف کنید:

    state = {
        purchasing: false
    }

همچنین در انتهای همین فایل یک شرط if برای نمایش Spinner داریم:

        if ( this.state.loading ) {
            orderSummary = <Spinner />;
        }

این کد را نیز از این فایل حذف کنید چرا که دیگر نیازی به آن نداریم. همچنین برای مدیریت خطا از کد زیر کمک گرفته بودیم:

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

        if ( this.props.ings ) {
            burger = (
                <Aux>
                    <Burger ingredients={this.props.ings} />
                    <BuildControls
                        ingredientAdded={this.props.onIngredientAdded}
                        ingredientRemoved={this.props.onIngredientRemoved}
                        disabled={disabledInfo}
                        purchasable={this.updatePurchaseState(this.props.ings)}
                        ordered={this.purchaseHandler}
                        price={this.props.price} />
                </Aux>
            );
            orderSummary = <OrderSummary
                ingredients={this.props.ings}
                price={this.props.price}
                purchaseCancelled={this.purchaseCancelHandler}
                purchaseContinued={this.purchaseContinueHandler} />;
        }

به عبارت دیگر اگر خطایی در کار نبود مقدار متغیر burger را به حالت عادی نمایش می دهیم و در غیر این صورت، متن Ingredients can't be loaded را نمایش می دهیم. البته ما در انتهای همین فایل دستور زیر را داریم:

export default connect(mapStateToProps, mapDispatchToProps)(withErrorHandler( BurgerBuilder, axios ));

بر اساس این کد متوجه می شویم که می توانیم خطا ها را با همان HOC خودمان مدیریت کنیم (منظور من withErrorHandler است) بنابراین از هر قسمتی از برنامه که درخواستی ارسال شود و با خطا مواجه شود با همین HOC مدیریت خواهد شد.

برای ادامه کار ابتدا به فایل burgerBuilder.js درون پوشه reducers رفته و مقدار initialState را به شکل زیر تغییر دهید:

const initialState = {
    ingredients: null,
    totalPrice: 4,
    error: false
};

با این کار مطمئن می شویم که خصوصیت error را درون redux داریم و از آنجا خطاهای احتمالی را چک می کنیم. در مرحله بعد باید در هنگام بروز خطا از سمت سرور یک action خاص را dispatch کنیم بنابراین وارد فایل actionTypes.js شده و مورد زیر را به آن اضافه کنید:

export const ADD_INGREDIENT = 'ADD_INGREDIENT';
export const REMOVE_INGREDIENT = 'REMOVE_INGREDIENT';
export const SET_INGREDIENTS = 'SET_INGREDIENTS';
export const FETCH_INGREDIENTS_FAILED = 'FETCH_INGREDIENTS_FAILED';

قطعا نام FETCH_INGREDIENTS_FAILED دلخواه است و به سلیقه شما بستگی دارد. حالا به burgerBuilder.js (درون پوشه actions) می رویم و یک actionCreator دیگر را تعریف می کنیم:

export const setIngredients = ( ingredients ) => {
    return {
        type: actionTypes.SET_INGREDIENTS,
        ingredients: ingredients
    };
};

export const fetchIngredientsFailed = () => {
    return {
        type: actionTypes.FETCH_INGREDIENTS_FAILED
    };
};

export const initIngredients = () => {
    return dispatch => {
        axios.get( 'https://react-my-burger.firebaseio.com/ingredients.json' )
            .then( response => {
               dispatch(setIngredients(response.data));
            } )
            .catch( error => {

            } );
    };
};

همانطور که می بینید نام آن را fetchIngredientsFailed قرار داده ام و آن را بالاتر از initIngredients نوشته ام. حالا برای تکمیل کد آن را درون initIngredients قرار می دهیم:

export const initIngredients = () => {
    return dispatch => {
        axios.get( 'https://react-my-burger.firebaseio.com/ingredients.json' )
            .then( response => {
               dispatch(setIngredients(response.data));
            } )
            .catch( error => {
                dispatch(fetchIngredientsFailed());
            } );
    };
};

با انجام این کار می گوییم اگر عملیات دریافت از سرور موفقیت آمیز بود action مورد نظر ما را ارسال کن و اگر به خطا برخورد کردیم action متناسب با خطا را ارسال کن. در قسمت بعد به سراغ reducer خواهیم رفت تا action های ارسال شده از این فایل را در آنجا دریافت و مدیریت کنیم.

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

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