Software developer

Developer of webapps and more since 2016

04 Apr 2020

557
Redux tips

Tags

Introduction

I've worked with React/Redux apps for a couple of years and I'd like to share some tips and tricks I made and discovered to manage loading and errors states and trigger ui changes across the app. Note: this article was written before the creation of Redux Toolkit.

Loading and Error reducer

To display a loader in the app when fetching data from the server, name the constants in a specific way and use a reducer to manage a loading state for every action matching the pattern in the store.


// reducers/loading.ts

export default function loading(state: any = {}, action: any) {
    const { type } = action;
    const matches = /(.*)_(REQUEST|SUCCESS|FAILURE)/.exec(type);

    // not a *_REQUEST / *_SUCCESS /  *_FAILURE actions, so we ignore them
    if (!matches) { return state; }

    const [, requestName, requestState] = matches;
    return {
        ...state,
        // Store whether a request is happening at the moment or not
        // e.g. will be true when receiving GET_TODOS_REQUEST
        //      and false when receiving GET_TODOS_SUCCESS / GET_TODOS_FAILURE
        [requestName]: requestState === 'REQUEST',
    };
}


Name the constants like this :

const REQUEST = '_REQUEST';
const SUCCESS = '_SUCCESS';
const FAILURE = '_FAILURE';

export const SOMETHING = 'SOMETHING';
export const SOMETHING_REQUEST = SOMETHING + REQUEST;
export const SOMETHING_SUCCESS = SOMETHING + SUCCESS;
export const SOMETHING_FAILURE = SOMETHING + FAILURE;

There is now a new entry in the store named 'loading' at root level, accessible by a state selector :

// helpers/selectors.ts

export const createLoadingSelector = (actions: Array<string>) => (state: StoreState) => {
    // returns true only when all actions is not loading
    return _(actions)
        .some((action) => _.get(state, action));
};

It is usable in mapStateProps :

// Flower.tsx

import { createLoadingSelector } from '../helpers/selectors'; 
import { SOMETHING, SOMETHING_ELSE } from '../constants';

export default connect(
    (state: StoreState) => ({
        somethingLoading: createLoadingSelector([SOMETHING])(state.loading),
        someOrElse: createLoadingSelector([SOMETHING, SOMETHING_ELSE])(state.loading)
    }),
    (dispatch : Dispatch) => ({
        dispatch
    )}
 )
(
    class Flower extends React.Component<Props, State>{ 
       ...
    }
)

Every time the action will come thru the state "REQUEST", it will trigger a loading state in the store that can be used in the component, displaying a loader or so.

'somethingLoading' will be true every time the constant SOMETHING come thru the reducer 'someOrElse' will be true every time the constant SOMETHING OR the constant SOMETHING_ELSE will come thru the reducer.

Ui Actions

Triggering a ui animation or a modal to open from anywhere in the app can be done by using these actions with the reducer below :

// constants/ui.ts

export const UI_UPDATE = 'UI_UPDATE';
export const MENU_IS_OPEN = 'MENU_IS_OPEN';

// actions/ui.ts

export const changeMenuOpenState = (isOpen: boolean) => ({
    type: UI_UPDATE,
    value: isOpen,
    path : MENU_IS_OPEN
});


Ui reducer

// reducers/ui.ts
import { MENU_IS_OPEN, UI_UPDATE } from '../constants';

const initialState = {
    [MENU_IS_OPEN]: false
};

export interface UiState = ReturnType<typeof initialState>;

export default function ui(state: UiState = initialState, action: UiAction) {
    const newState = { ...state };
    if (action.type !== UI_UPDATE) { return state; }
    return _(newState).set(action.path, action.value).value();

Using this action in the components will open or close the menu

Store entity wrapper

A wrapper around a store entity is useful for transforming the store data before display. The data being stored in the store, for displaying or calculating formulas you may want to create a wrapper (OOP class) with functions in the store. Currently, this is not recommended as the store state must be seriablizable. To bypass this safety restriction, put instead the raw data in the store and use mapStateProps to create the helper.

Lets say you need a shopping cart and you need to know if it is empty or you need to know the amount the customer will pay, example for a cinema distance selling :

// models/shoppingCart.ts

class ShoppingCart {
    items: Array<ShoppingCartItem>;
    constructor(items?: Array<ShoppingCartItem>) {
        this.items = items ? items : [];
    }

    /*
     * Know if it is empty
     */
    isEmpty(): boolean {
        return this.items.length === 0;
    }

    /*
     * ... is full (amount of tickets equals 10)
     */
    isFull(): boolean {
        return this.getTotalTicketsCount() === 10;
    }

    /*
     * Know the number of tickets of the shopping Cart
     */
    getTotalTicketsCount() {
        return this.items
            .filter((item: ShoppingCartItem) =>
                item.constructor === ShoppingCartItemTicket
            ).reduce((accu: number, item: any) => item.qty ? accu + item.qty : accu, 0);
    }

    size() {
        return this.items.length;
    }

    getTotalWithTaxes() {
        return this.items.reduce((accu, item) => {
            return accu + item.getSubtotalTtc();
        }, 0);
    }
}


//component.tsx


export default connect(
  (state: StoreState) => ({
     cart : new ShoppingCart(state.shoppingCartItems),
  }),
  (dispatch: Dispatch) => ({})
)(
    class Cmp extends React.Component {
        render {
            const { shoppingCart } = this.props;
            return (
                 <div>
                        <span> 
                                      {shoppingCart.isEmpty() 
                                      ? 'The cart is empty' 
                                      : shoppingCart.isFull() 
                                      ? 'The cart is full'
                                      : 'There are currently ' + shoppingCart.size() + ' items in the cart' } 
                        </span>

                        <span>Total including taxes: {shoppingCart.getTotalWithTaxes()}€</span>
                 </div>
           )
       }
    }
);