اتصال کامپوننت BurgerBuilder به store

Connecting the BurgerBuilder component to store

22 بهمن 1399
اتصال کامپوننت BurgerBuilder به store

حالا که کار reducer را فعلا تمام کرده ایم باید کامپوننت BurgerBuilder را به Store متصل کنیم. در جلسات قبل store را در index.js ایجاد کرده و کل برنامه را درون آن قرار دادیم بنابراین باید مستقیم به فایل BurgerBuilder.js برویم. حتما می دانید که شروع فرآیند اتصال، با تابع connect از پکیج react-redux انجام می شود بنابراین اول از همه آن را وارد BurgerBuilder می کنیم:

import { connect } from 'react-redux';

ممکن است بخواهید مستقیما به دستور export بروید و آن را درون connect قرار دهید اما مشکلی وجود دارد. ما یک HOC برای مدیریت خطاها ساخته بودیم که دستور export را در بر گرفته است:

export default withErrorHandler( BurgerBuilder, axios );

چطور می توانیم connect را نیز درون آن بگنجانیم؟ واقعیت این است که هیچ مشکلی وجود ندارد! در واقع ما می توانیم هر تعداد HOC که خواسته باشیم تعریف کنیم. connect در پشت صحنه فقط چند prop را به کامپوننت های درونش منتقل می کند، همین! بنابراین تا زمانی که درون HOC های خودمان هم prop ها را پاس داده باشیم هیچ مشکلی پیش نمی آید چرا که HOC ما prop ها را به HOC بعدی و سپس آن HOC هم به HOC بعدی و الی آخر پاس می دهند تا به هدف مورد نظر برسند.

البته قبل از انجام هر کاری باید دو ثابت خود را مثل همیشه تعریف کنیم تا مشخص شود چه چیزی به عنوان prop به فایل ما ارسال خواهد شد:

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

ثابت اول همان mapStateToProps است که یک تابع بوده و به صورت خودکار State برنامه را دریافت می کند (به عنوان پارامتر). همانطور که گفتیم درون این تابع، شیء ای برگردانده می شود که نشان می دهد هر قسمت از state به شکل چه prop ای دریافت شوند. مثلا در کد بالا ings (مخفف ingredients) نام انتخابی من است که قسمت state.ingredients را به عنوان prop در اختیار کامپوننت ما می گذارد.

ثابت دوم mapDispatchToProps است و از آنجایی که در آن از action.Type استفاده می کنیم باید ابتدا actionType ها را وارد این فایل کنیم:

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

حالا می توانیم با خیال راحت بگوییم:

const mapDispatchToProps = dispatch => {
    return {
        onIngredientAdded: (ingName) => dispatch({type: actionTypes.ADD_INGREDIENT, ingredientName: ingName})
    }
}

تمام این کدها را در همین فصل کار کرده ایم برای همین فکر نمی کنم چیز جدیدی وجود داشته باشد اما آیا متوجه حضور خصوصیت ingredientName شدید؟ آیا می دانید چرا چنین خصوصیتی را به action خود اضافه کرده ایم؟ اگر یادتان باشد درون فایل reducer.js گفته بودیم که:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.ADD_INGREDIENT:
            return {
                ...state,
                ingredients: {
                    ...state.ingredients,
                    [action.ingredientName]: state.ingredients[action.ingredientName] + 1
                }
            };
        case actionTypes.REMOVE_INGREDIENT:
            return {
                ...state,
                ingredients: {
                    ...state.ingredients,
                    [action.ingredientName]: state.ingredients[action.ingredientName] - 1
                }
            };
        default:
            return state;
    }
};

یعنی هر دو کیس اضافه کردن و حذف کردن محتویات همبرگر باید نام آن محتویات را دریافت کنند که همان ingredientName است و از طریق action دریافت می شود. در واقع من انتظار دارم پارامتری به نام ingName به onIngredientAdded پاس داده شود، گرچه که هنوز کدهایش را ننوشته ایم. حالا با همین استراتژی می توانیم مورد حذف محتویات را هم بنویسیم:

const mapDispatchToProps = dispatch => {
    return {
        onIngredientAdded: (ingName) => dispatch({type: actionTypes.ADD_INGREDIENT, ingredientName: ingName}),
        onIngredientRemoved: (ingName) => dispatch({type: actionTypes.REMOVE_INGREDIENT, ingredientName: ingName})
    }
}

حالا باید connect را به دستور export اضافه کنیم:

const mapStateToProps = state => {
    return {
        ings: state.ingredients,
        price: state.totalPrice
    };
}

const mapDispatchToProps = dispatch => {
    return {
        onIngredientAdded: (ingName) => dispatch({type: actionTypes.ADD_INGREDIENT, ingredientName: ingName}),
        onIngredientRemoved: (ingName) => dispatch({type: actionTypes.REMOVE_INGREDIENT, ingredientName: ingName})
    }
}

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

همانطور که در جلسات قبل عمل کردیم باید دو ثابت mapStateToProps و mapDispatchToProps را به connect پاس داده و به عنوان آرگومان دوم کل HOC خودمان را برایش ارسال کرده ایم. قبلا هم گفتیم که متعدد بودن HOC ها مشکلی در کار برنامه به وجود نمی آورد. حالا ما از طریق BurgerBuilder به این خصوصیات و state دسترسی داریم بنابراین می توانیم از آن ها استفاده کنیم.

برای شروع کار ابتدا ingredient را از state این کامپوننت حذف کنید تا بدانیم که از state سراسری در redux استفاده می کنیم:

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

در مرحله بعد باید به متد render برویم و کد زیر را:

    render () {
        const disabledInfo = {
            ...this.state.ingredients
        };
// بقیه کدها //

به کد زیر تغییر دهید:

    render () {
        const disabledInfo = {
            ...this.props.ings
        };

دلیل این کار نیز مشخص است؛ ما دیگر از state خود react استفاده نمی کنیم بلکه در حال کار با redux هستیم. به عبارت دیگر connect دو ثابت ما را گرفته و state را به صورت prop های تعریف شده در شیء mapStateToProps به کامپوننت ما پاس می دهد بنابراین state به props و نام خصوصیت نیز به نام تعریف شده در شیء mapStateToProps تغییر می کند (همان ing).

در واقع در هر قسمتی از این فایل که از state.ingredients استفاده شده باشد، شما باید آن را به props.ings تغییر دهید. مثلا در کد زیر نیز شاهد چنین چیزی هستیم:

if (this.state.ingredients) {
    burger = (
        <Aux>
            <Burger ingredients={this.state.ingredients} />
            <BuildControls
                ingredientAdded={this.addIngredientHandler}
                // بقیه کدها

شرط if دارای state.ingredients است، همچنین برخی از موارد دیگر مثل ingredients ای که به صورت prop به <Burger> پاس داده شده است، بنابراین آن را به props.ings تغییر می دهیم:

if (this.props.ings) {
    burger = (
        <Aux>
            <Burger ingredients={this.props.ings } />
            <BuildControls
                ingredientAdded={this.addIngredientHandler}
                // بقیه کدها

بقیه موارد را نیز پیدا کرده و خودتان آن ها را تغییر دهید.

در مرحله بعد باید به سراغ کد زیر برویم:

if (this.state.ingredients) {
    burger = (
        <Aux>
            <Burger ingredients={this.state.ingredients} />
            <BuildControls
                ingredientAdded={this.addIngredientHandler}
                ingredientRemoved={this.removeIngredientHandler}
                disabled={disabledInfo}
                purchasable={this.state.purchasable}
                ordered={this.purchaseHandler}
                price={this.state.totalPrice} />
        </Aux>
    );

در این کد دیگر نیازی به استفاده از توابع addIngredientHandler و removeIngredientHandler نداریم. در واقع می توانیم هر جفت آن ها را حذف کنیم اما از آنجایی که هنوز مدیریت قیمت همبرگر در آن ها انجام می شود به آن ها دست نمی زنم. به جای آن در کد بالا و برای prop های ingredientAdded و ingredientRemoved از کدهای جدید خودمان استفاده می کنم:

if (this.state.ingredients) {
    burger = (
        <Aux>
            <Burger ingredients={this.state.ingredients} />
            <BuildControls
                ingredientAdded={this.props.onIngredientAdded}
                ingredientRemoved={this.props.onIngredientRemoved}
                disabled={disabledInfo}
                purchasable={this.state.purchasable}
                ordered={this.purchaseHandler}
                price={this.state.totalPrice} />
        </Aux>
    );

البته کار ما هنوز تمام نشده است. همانطور که به یاد دارید onIngredientAdded و onIngredientRemoved هر دو نیاز به یک پارامتر به نام ingName داشتند. همچنین اگر به کامپوننت BuildControls.js برویم، یادمان می آید که یک پارامتر به نام ctrl.type را به آن داده ایم:

const buildControls = (props) => (
    <div className={classes.BuildControls}>
        <p>Current Price: <strong>{props.price.toFixed(2)}</strong></p>
        {controls.map(ctrl => (
            <BuildControl 
                key={ctrl.label} 
                label={ctrl.label}
                added={() => props.ingredientAdded(ctrl.type)}
                removed={() => props.ingredientRemoved(ctrl.type)}
                disabled={props.disabled[ctrl.type]} />
        ))}
        <button 
            className={classes.OrderButton}
            disabled={!props.purchasable}
            onClick={props.ordered}>ORDER NOW</button>
    </div>
);

خوشبختانه اگر به همین فایل نگاه کنیم متوجه می شویم که ctrl.type دقیقا همان مقادیری را دارد که ما می خواهیم:

const controls = [
    { label: 'Salad', type: 'salad' },
    { label: 'Bacon', type: 'bacon' },
    { label: 'Cheese', type: 'cheese' },
    { label: 'Meat', type: 'meat' },
];

ما عینا همین type ها را در فایل reducer می خواهیم بنابراین مشکلی نیست و می توانیم کدها را به همین شکل ذخیره کنیم. در صورتی که برنامه ما متفاوت بود باید کدها را دوباره می نوشتیم و برنامه را به شکلی تغییر می دادیم که مشکلی ایجاد نکند.

اگر الان به مرورگر مراجعه کنید، دکمه های اضافه کردن و حذف محتویات را بدون مشکل می بینید که خبر خوبی است اما هنوز کدی برای تغییر قیمت ننوشته ایم و قیمت هیچ وقت تغییر نخواهد کرد. همچنین دکمه ORDER NOW نیز همیشه غیر فعال است. در جلسات بعدی هر دوی این موارد را تصحیح خواهیم کرد.

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

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