ترکیب چندین reducer

Combining Multiple Reducers

12 مرداد 1399
ترکیب چندین reducer

در جلسه قبل برایتان توضیح دادم که در پروژه های بزرگ تعداد action های ما زیاد می شود بنابراین برای کم کردن احتمال خطای تایپی و منظم نگه داشتن آن ها، یک فایل به نام actions.js را ایجاد کرده و هر کدام از action.type ها را در یک ثابت قرار دادیم اما یکی دیگر از مسائلی که در پروژه های بزرگتر رخ می دهد استفاده از چندین reducer است. البته من در جلسات آشنایی با redux به شما گفته بودم که در redux فقط یک reducer داریم و این حرف درست است، تمام action های ما نهایتا به یک reducer می رسند اما پکیج redux ابزاری به ما می دهد که با استفاده از آن می توانیم چندین reducer را درون یک reducer قرار دهیم.

بنابراین در پشت صحنه و هنگام اجرای کدها فقط یک reducer وجود خواهد داشت اما در هنگام توسعه و کدنویسی توسط خود ما، می توانیم چندین reducer داشته باشیم. در حال حاضر reducer ما دارای یک دستور switch نستبا طولانی است:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.INCREMENT:
            const newState = Object.assign({}, state);
            newState.counter = state.counter + 1;
            return newState;
        // return {
        //     ...state,
        //     counter: state.counter + 1
        // }
        case actionTypes.DECREMENT:
            return {
                ...state,
                counter: state.counter - 1
            }
        case actionTypes.ADD:
            return {
                ...state,
                counter: state.counter + action.val
            }
        case actionTypes.SUBTRACT:
            return {
                ...state,
                counter: state.counter - action.val
            }
        case actionTypes.STORE_RESULT:
            return {
                ...state,
                results: state.results.concat({ id: new Date(), value: state.counter })
            }
        case actionTypes.DELETE_RESULT:
            const updatedArray = state.results.filter(result => result.id !== action.resultElId);
            return {
                ...state,
                results: updatedArray
            }
    }
    return state;
};

با اینکه برنامه ما فقط چهار دکمه دارد که یک شمارنده را تغییر می دهند و بسیار کوچک است، این دستور switch نسبتا طولانی است. حالا تصور کنید که برنامه ما واقعی باشد! اگر بخواهیم 50 شرط مختلف را بررسی کنیم به یک دستور Switch بسیار طولانی می رسیم که اجرای آن وقت گیر بوده و کمی برنامه را کُند می کند.

بنابراین داشتن چند reducer کمک بزرگی محسوب می شود. مثلا در برنامه ساده ما باید به اجزای reducer نگاه کنید تا بتوانیم آن را به شکلی منطقی تغییر دهیم. به state نگاه کنید:

const initialState = {
    counter: 0,
    results: []
}

مشخص است که می توانیم دو reducer داشته باشیم: یکی برای مدیریت counter و دیگری برای مدیریت results! برای انجام این کار وارد پوشه Store شده و یک پوشه جدید به نام reducers ایجاد کنید. حالا درون این پوشه دو فایل به نام های counter.js و result.js بسازید. در قدم اول تمام کدهای درون reducer.js را کپی کرده و در counter.js (درون پوشه reducers) کپی کنید تا از بالا به پایین تک تک قسمت ها را ویرایش کنیم.

اول دستور import را تصحیح می کنیم:

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

سپس initialState را نیز تصحیح می کنیم:

const initialState = {
    counter: 0
};

فایل counter.js (درون پوشه reducer) مختص به شمارنده است بنابراین نیازی به results نداشته و آن را حذف کرده ایم.

در مرحله بعد case های مربوط به result (یعنی STORE_RESULT و DELETE_RESULT) را حذف می کنیم چرا که در این فایل نیازی به آن ها نیست:

const reducer = ( state = initialState, action ) => {
    switch ( action.type ) {
        case actionTypes.INCREMENT:
            const newState = Object.assign({}, state);
            newState.counter = state.counter + 1;
            return newState;
        case actionTypes.DECREMENT:
            return {
                ...state,
                counter: state.counter - 1
            }
        case actionTypes.ADD:
            return {
                ...state,
                counter: state.counter + action.val
            }
        case actionTypes.SUBTRACT:
            return {
                ...state,
                counter: state.counter - action.val
            }
    }
    return state;
};

به همین سادگی reducer ما در این قسمت تکمیل شد! البته شما می توانید نام این تابع را از reducer به هر چیز دیگری تغییر دهید یا می توانید مثل من به آن دست نزنید.

در مرحله بعد دوباره کدهای reducer.js (فایل reducer قدیمی) را کپی کرده اما این بار آن را درون result.js کپی کنید، سپس با هم وارد تغییرات می شویم. ابتدا باید دستور import به شکل زیر تصحیح شود:

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

این بار state را به شکل زیر تغییر می دهیم:

const initialState = {
    results: []
};

مطمئن هستم که دلیل این کار را متوجه شده اید. همچنین این بار تمام case های مربوط به counter را حذف می کنم:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.STORE_RESULT:
            return {
                ...state,
                results: state.results.concat({ id: new Date(), value: state.counter })
            }
        case actionTypes.DELETE_RESULT:
            const updatedArray = state.results.filter(result => result.id !== action.resultElId);
            return {
                ...state,
                results: updatedArray
            }
    }
    return state;
};

نکته جالب اینجاست که در این فایل هنوز هم به state.counter دسترسی داریم چرا که در نهایت تمام reducer ها با هم ادغام خواهند شد و state آن ها نیز یکی خواهد شد بنابراین مشکلی از بابت state.counter نخواهیم داشت (البته از نظر پکیج redux نه از نظر برنامه خودمان!). حالا می توانید فایل قدیمی reducer.js را به طور کامل حذف کنید.

اگر یادتان باشد درون فایل index.js یک دستور برای وارد کردن reducer.js داشتیم:

import reducer from './store/reducer';

اما حالا دو reducer مختلف داریم و دستور بالا خطا است. برای حل این مشکل هر دو فایل reducer را به جای این دستور وارد می کنیم:

import counterReducer from './store/reducers/counter';
import resultReducer from './store/reducers/result';

شما می توانید هر نام دیگری را برای آن ها انتخاب کنید، counterReducer و resultReducer قابل تغییر هستند. البته برای ادغام کردن آن ها به یک تابع کمکی به نام combineReducers نیاز دارم بنابراین آن را نیز از پکیج redux وارد می کنم:

import { createStore, combineReducers } from 'redux';

حالا قبل از ساختن store می گوییم:

const rootReducer = combineReducers({
    ctr: counterReducer,
    res: resultReducer
});

همانطور که گفتم این تابع یک شیء جاوا اسکریپتی را به عنوان پارامتر می گیرد و در این شیء جاوا اسکریپتی باید reducer های خود را با نام دلخواه وارد کنید. مثلا می توانید به جای ctr نام counter یا هر چیز دیگری قرار دهید. همچنین نام ثابت اصلی (rootReducer) نیز به سلیقه شما بستگی داشته و قابل تغییر است.

حالا می توانیم rootReducer را به دستور createStore پاس بدهیم:

const store = createStore(rootReducer);

برای شفاف تر شدن کار کل محتوای فایل index.js خود را برایتان کپی می کنم:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';

import counterReducer from './store/reducers/counter';
import resultReducer from './store/reducers/result';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

const rootReducer = combineReducers({
    ctr: counterReducer,
    res: resultReducer
});

const store = createStore(rootReducer);

ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();

تا اینجای کار از نظر پکیج redux مشکلی وجود ندارد و کدها سالم اجرا خواهند شد اما برنامه react ما به ما خطا می دهد. چرا؟ به کد زیر نگاه کنید:

return (
    <div>
        <CounterOutput value={this.props.ctr} />
        <CounterControl label="Increment" clicked={this.props.onIncrementCounter} />
        <CounterControl label="Decrement" clicked={this.props.onDecrementCounter} />
        <CounterControl label="Add 10" clicked={this.props.onAddCounter} />
        <CounterControl label="Subtract 15" clicked={this.props.onSubtractCounter} />
        <hr />
        <button onClick={this.props.onStoreResult}>Store Result</button>
        <ul>
            {this.props.storedResults.map(strResult => (
                <li key={strResult.id} onClick={() => this.props.onDeleteResult(strResult.id)}>{strResult.value}</li>
            ))}
        </ul>
    </div>
);

ما در این کد تابع map را روی مقدار storedResults صدا زده ایم اما دیگر storedResults ای وجود ندارد:

const mapStateToProps = state => {
    return {
        ctr: state.counter,
        storedResults: state.results
    };
};

در واقع با ادغام reducer ها کد بالا کار نخواهد کرد. من گفتم زمانی که reducer ها را ادغام کنیم یک state خواهیم داشت و state ها نیز در هم ادغام می شوند اما ادغام شدن این state ها به صورت یک state سراسری خواهد بود که از طریق property های rootReducer قابل دسترسی است، یعنی این کد در index.js:

const rootReducer = combineReducers({
    ctr: counterReducer,
    res: resultReducer
});

به عبارت دیگر برای دسترسی به counter در State خود باید به state.ctr.counter دسترسی پیدا کنیم. این ctr همان ctr در کد بالا است. بنابراین به فایل counter.js بروید و مثل من mapStateToProps را تغییر دهید:

const mapStateToProps = state => {
    return {

        ctr: state.ctr.counter,
        storedResults: state.res.results
    }
};

حالا اگر کدها را ذخیره کنیم و به مرورگر برویم با مشکل جدیدی روبرو می شویم. با کلیک روی دکمه Store Result عدد شمارنده ذخیره نمی شود. این مشکل مربوط به کد زیر در result.js است:

case actionTypes.STORE_RESULT:
    return {
        ...state,
        results: state.results.concat({ id: new Date(), value: state.counter })
    }

از نظر react دیگر state.counter ای وجود ندارد! احتمالا با خودتان بگویید اگر ctr را به آن اضافه کنیم مشکل حل می شود:

case actionTypes.STORE_RESULT:
    return {
        ...state,
        results: state.results.concat({ id: new Date(), value: state.ctr.counter })
    }

اما با انجام این کار مشکلات بیشتری درست کرده و تعداد خطا ها بیشتر می شود. مشکل اینجاست که این reducer ما (فایل result.js) به state سراسری دسترسی ندارد، بلکه فقط به state خودش دسترسی خواهد داشت:

const initialState = {
    results: []
};

به نظر شما راه حل چیست؟

بهترین راه حل این است که state را به همراه اطلاعات دیگر درون action ها ارسال کنیم. بنابراین:

const reducer = ( state = initialState, action ) => {
    switch ( action.type ) {
        case actionTypes.STORE_RESULT:
            return {
                ...state,
                results: state.results.concat({id: new Date(), value: action.result})
            }
        case actionTypes.DELETE_RESULT:
            // const id = 2;
            // const newArray = [...state.results];
            // newArray.splice(id, 1)
            const updatedArray = state.results.filter(result => result.id !== action.resultElId);
            return {
                ...state,
                results: updatedArray
            }
    }
    return state;
};

یعنی من انتظار دارم که action.result را دریافت کنم بنابراین باید به Counter.js برویم و این مورد را از mapDispatchToProps پاس بدهیم:

onStoreResult: (result) => dispatch({type: actionTypes.STORE_RESULT, result: result}),

حالا به قسمت JSX می رویم تا کد را تکمیل کنیم:

<button onClick={() => this.props.onStoreResult(this.props.ctr)}>Store Result</button>

حالا کد ما تکمیل شده است و بدون خطا در مرورگر اجرا خواهد شد.

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

دیدگاه‌های شما

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