Redux پیشرفته در پروژه همبرگرساز – ثبت سفارشات و redirect

Advanced Redux in Burger Builder Project - Order Registration and Redirection

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

تا این قسمت کارایی زیادی به برنامه خود اضافه کرده ایم اما همانطور که گفتم در جلسه قبل با مشکلی روبرو شدیم. پس از ثبت سفارش کاربر به صفحه اصلی redirect نمی شود و در همان صفحه سفارش باقی می ماند. یکی از راه های انجام این کار پاس دادن یک نمونه از history درون پکیج Router به تابع purchaseBurger است. این تابع در فایل order.js در پوشه actions قرار دارد:

export const purchaseBurger = ( orderData ) => {
    return dispatch => {
        dispatch( purchaseBurgerStart() );
        axios.post( '/orders.json', orderData )
            .then( response => {
                console.log( response.data );
                dispatch( purchaseBurgerSuccess( response.data.name, orderData ) );
            } )
            .catch( error => {
                dispatch( purchaseBurgerFail( error ) );
            } );
    };
};

اگر history را به صورت یک پارامتر به این تابع پاس بدهیم می توانیم در هنگام dispatch کردن purchaseBurgerSuccess متد push را نیز صدا بزنیم تا کاربر redirect شود. این روش مشکلی ندارد اما من دوست دارم بیشتر روی redux تمرکز کنیم بنابراین از روشی استفاده می کنم که با redux درگیر باشد.

برای شروع کار به actionTypes.js رفته و یک action دیگر را تعریف می کنم:

export const PURCHASE_INIT = 'PURCHASE_INIT';

سپس در پوشه actions فایل order.js را پیدا کنید و وارد آن شوید تا actionCreator آن را ایجاد کنیم:

export const purchaseInit = () => {
    return {
        type: actionTypes.PURCHASE_INIT
    };
};

بنابراین فقط action مورد نظر را ارسال کرده ایم. در مرحله بعد وارد index.js می شویم (پوشه actions) تا purchaseInit را وارد آن کنیم:

export {
    addIngredient,
    removeIngredient,
    initIngredients
} from './burgerBuilder';
export {
    purchaseBurger,
    purchaseInit
} from './order';

اگر یادتان باشد index.js نقطه ای مرکزی در برنامه ما بود که از طریق آن می توان به تمام action creator ها دسترسی پیدا کرد. در مرحله بعد به فایل Checkout.js در پوشه ContactData می رویم و componentWillMount را به آن اضافه می کنیم. قرار است خصوصیتی داشته باشیم که نشان دهنده شروع عملیات خرید باشد و سپس درون componentWillMount به false تغییر کند تا نشان دهد که خرید تمام شده است. زمانی که این خصوصیت به false تغییر پیدا کند، کاربر redirect خواهد شد.

بنابراین ابتدا در همان فایل Checkout.js باید mapDispatchToProps را تعریف کنیم:

const mapDispatchToProps = dispatch => {
    return {
        onInitPurchase : () => dispatch()
    }
}

در واقع برای خصوصیت onInitPurchase یک تابع را در نظر گرفته ایم که باید purchaseInit را dispatch کند به همین خاطر نیاز به دسترسی به فایل index.js داریم بنابراین دستور import آن را وارد همین فایل می کنیم:

import * as actions from '../../store/actions/index';

حالا به mapDispatchToProps برمی گردیم تا purchaseInit را به آن بدهیم:

const mapDispatchToProps = dispatch => {
    return {
        onInitPurchase : () => dispatch(actions.purchaseInit())
    }
}

حتما یادتان باشد که آن را به connect بدهید:

export default connect( mapStateToProps, mapDispatchToProps )( Checkout );

حالا در همین فایل componentWillMount را در ابتدای کلاس Checkout تعریف می کنیم:

class Checkout extends Component {

    componentWillMount () {
        this.props.onInitPurchase();
    }

    // بقیه کدها

حالا onInitPurchase را dispatch کرده ایم بنابراین باید به reducer ها برویم تا action را دریافت کنیم و بگوییم با دریافت آن چه اتفاقی بیفتد. بنابراین باید فایل order.js درون پوشه reducers را باز کنید و case جدید خودمان را به دستور switch بدهید. البته قبل از آن باید خصوصیتی به نام purchased را به State این فایل اضافه کنیم:

const initialState = {
    orders: [],
    loading: false,
    purchased: false
};

این خصوصیت مشخص می کند که آیا سفارش تمام شده است یا خیر. در ابتدا روی false خواهد بود چرا که کاربر از ابتدا در حال خرید نیست مگر اینکه خودش فرآیند را شروع کند. سپس زمانی که خرید شروع می شود action مورد نظر ارسال شده و این خصوصیت true می شود. در نهایت با تکمیل سفارش باید دوباره آن را روی False بگذاریم. بنابراین می توان گفت:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.PURCHASE_INIT:
            return {
                ...state,
                purchased: false
            }
            // بقیه کدها

یعنی زمانی که PURCHASE_INIT به سمت reducer ارسال شد و آن را دریافت کردیم، خصوصیت purchased را روی false می گذاریم. همچنین باید PURCHASE_BURGER_SUCCESS را به شکلی ویرایش کنیم که purchased در آن برابر با true شود به دلیل اینکه PURCHASE_BURGER_SUCCESS فقط زمانی اجرا می شود که عملیات خرید موفقیت آمیز بوده باشد و آنجاست که می خواهیم کاربر را redirect کنیم.

        case actionTypes.PURCHASE_BURGER_SUCCESS:
            const newOrder = {
                ...action.orderData,
                id: action.orderId
            }
            return {
                ...state,
                loading: false,
                purchased: true,
                order: state.orders.concat(newOrder);
            }
        // بقیه کدها

در مرحله بعد به سادگی به فایل Checkout.js برمی گردیم. در قسمت mapStateToProps به purchased نیاز داریم تا بتوانیم آن را در منطق Redirect خود استفاده کنیم بنابراین آن را اضافه می کنیم:

const mapStateToProps = state => {
    return {
        ings: state.burgerBuilder.ingredients,
        purchased: state.order.purchased
    }
};

 سپس به متد render می رویم و درون شرط if ثابت دیگری تعریف می کنیم:

    render () {
        let summary = <Redirect to="/" />
        if ( this.props.ings ) {
            const purchasedRedirect = this.props.purchased ? <Redirect to="/"/> : null;

یعنی اگر purchased برابر true بود (سفارش با موفقیت تکمیل شده بود) کاربر را به صفحه اصلی redirect کن، در غیر این صورت هنوز در فرآیند خرید هستیم بنابراین آن را روی null می گذارم. در نهایت هم purchasedRedirect را درون summary نمایش می دهیم:

    render () {
        let summary = <Redirect to="/" />
        if ( this.props.ings ) {
            const purchasedRedirect = this.props.purchased ? <Redirect to="/"/> : null;
            summary = (
                <div>
                    {purchasedRedirect}
                    <CheckoutSummary
                        ingredients={this.props.ings}
                        checkoutCancelled={this.checkoutCancelledHandler}
                        checkoutContinued={this.checkoutContinuedHandler} />
                        // بقیه کدها

پس از ذخیره کدها به مرورگر می رویم و یک همبرگر سفارش می دهیم. با ثبت نهایی همبرگر به درستی redirect می شویم اما مشکلی وجود دارد. اگر بخواهیم دوباره همبرگری را سفارش دهیم هنوز به مرحله checkout و تکمیل فرم نرسیده، درجا redirect می شویم! سعی کنید قبل از اینکه من جواب را بگویم خودتان بفهمید مشکل از کجاست.

مشکل از اینجاست:

    componentWillMount () {
        this.props.onInitPurchase();
    }

componentWillMount نسبت به زمانی که ما می خواهیم دیر اجرا می شود و بنابراین onInitPurchase را نیز دیر اجرا می کند. با اینکه قبل از متد render اجرا می شود، از render شدن prop های قبلی جلوگیری نمی کند و در prop های قبلی مقدار purchased هنوز روی true است. بنابراین باید کل componentWillMount و همچنین کل mapDispatchToProps را از این فایل حذف کنید. سپس mapDispatchToProps را از دستور connect نیز حذف کنید. راه حل صحیح این است که onInitPurchase را درون BurgerBuilder.js در پوشه containers اجرا کنیم، دقیقا زمانی که روی Order Now کلیک می شود. مدیریت این عملیات با تابع زیر است:

    purchaseContinueHandler = () => {
        this.props.history.push('/checkout');
    }

یعنی ما را به checkout می برد. اینجا جایی است که باید onInitPurchase را صدا بزنیم:

    purchaseContinueHandler = () => {
        this.props.onInitPurchase();
        this.props.history.push('/checkout');
    }

با این کار دقیقا قبل از اینکه به صفحه checkout برویم، onInitPurchase را صدا می زنیم. البته از آنجایی که هنوز به آن دسترسی نداریم باید آن را در انتهای صفحه و در قسمت mapDispatchToProps صدا بزنیم:

const mapDispatchToProps = dispatch => {
    return {
        onIngredientAdded: (ingName) => dispatch(actions.addIngredient(ingName)),
        onIngredientRemoved: (ingName) => dispatch(actions.removeIngredient(ingName)),
        onInitIngredients: () => dispatch(actions.initIngredients()),
        onInitPurchase: () => dispatch(actions.purchaseInit())
    }
}

با این کار action ما آماده dispatch شدن می شود که dispatch شدن آن را بالاتر در purchaseContinueHandler انجام دادیم. البته من نام import خودم را از BurgerBuilderActions به actions خالی تغییر داده ام تا نام واضح تری داشته باشیم. برای انجام این کار باید دستور import خودتان را در همین فایل تغییر بدهید:

import * as actions from '../../store/actions/index';

حالا دوباره به مرورگر می رویم و یک همبرگر دیگر را سفارش می دهیم. پس از ثبت سفارش همبرگر اول می توانیم یک همبرگر دیگر را نیز سفارش دهیم بنابراین این مشکل برطرف شده است اما مشکل جدیدی اضافه شده است. پس از ثبت همبرگر اول، قیمت دیگر به صفر برنمی گردد بلکه روی قیمت همبرگر اول گیر می کند!

مشکل اینجاست که در burgerBuilder.js در پوشه reducers، قیمت را تنها زمانی به روز رسانی می کنیم که مخلفات و محتویات همبرگر تغییر کند (add و remove) اما باید آن را درون SET_INGREDIENTS نیز صدا بزنیم:

    case actionTypes.SET_INGREDIENTS:
            return {
                ...state,
                ingredients: {
                    salad: action.ingredients.salad,
                    bacon: action.ingredients.bacon,
                    cheese: action.ingredients.cheese,
                    meat: action.ingredients.meat
                },
                totalPrice: 4,
                error: false
            }
        // بقیه کدها

حالا که در اینجا نیز قیمت را به 4 تغییر دادم مشکل ما برطرف می شود. ما توانستیم قسمت ثبت سفارشات را تکمیل کنیم و حالا تنها قسمت باقی مانده، دریافت سفارشات از سرور های firebase است. این کار را در جلسه بعد انجام می دهیم.

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

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