Wednesday, March 27, 2019

For interface-free action props, use typeof

Instead of creating an interface for the actions that will be used by the component, just use typeof on actionCreators object. See line 54 and 63



A sample action creator (e.g., setColorTheme):

export const setColorTheme = (colorTheme: string): LoggedUserAction => ({
    type: LoggedUserActionType.LOGGED_USER__SET_COLOR_THEME,
    colorTheme
});

Tuesday, March 26, 2019

Without immer, with immer

Without immer, constructing immutables is error-prone:

increaseOrderQuantity(item) {
    const itemIndex = this.state.data.indexOf(item);

    this.setState(({data}) => ({
        data: [
            ...data.slice(0, itemIndex),
            {
                ...item,
                quantity: item.quantity + 1
            },
            ...data.slice(itemIndex + 1) // have made a mistake of forgetting to include + 1
        ]
    }));
}


With immer, less error-prone:

import produce from 'immer';

.
.
.


increaseOrderQuantity(item) {
    const itemIndex = this.state.data.indexOf(item);
    
    this.setState(produce(draft => {
        draft.data[itemIndex].quantity++;
    }));
}

Saturday, March 23, 2019

Don't use siloed mapDispatchToProps, use redux-thunk

This makes getLoggedUser functionality not accessible on all components but App component

export const incrementCounter = (n?: number): CounterAction => ({
    type: CounterActionType.COUNTER__INCREMENT,
    n
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
    incrementCounter: (n?: number) => dispatch(incrementCounter(n)),
    getLoggedUser: async () => {
        const userRequest = await fetch('https://jsonplaceholder.typicode.com/users/1');

        const {status} = userRequest;

        if (!(status === 200 || status === 301)) {
            throw new Error('HTTP Status: ' + status);
        }

        const {username} = await userRequest.json() as IUserDto;

        await dispatch(setLoggedUser(username));

        await dispatch(setColorTheme('blue'));

    }
});

export default connect(mapStateToProps, mapDispatchToProps)(hot(App));


To make getLoggedUser functionality accessible from other components, instead of defining it in mapDispatchToProps, define getLoggedUser outside and make it return a thunk that accepts dispatch parameter. A function returned from a function is called a thunk. Include the thunk creator on action creators.


export const incrementCounter = (n?: number): CounterAction => ({
    type: CounterActionType.COUNTER__INCREMENT,
    n
});

export const getLoggedUser = () => async (dispatch: Dispatch): Promise<void> =>
{
    const userRequest = await fetch('https://jsonplaceholder.typicode.com/users/1');

    const {status} = userRequest;

    if (!(status === 200 || status === 301)) {
        throw new Error('HTTP Status: ' + status);
    }

    const {username} = await userRequest.json() as IUserDto;

    await dispatch(setLoggedUser(username));

    await dispatch(setColorTheme('blue'));

};


const actionCreators = {
    incrementCounter, 
    getLoggedUser
};

export default connect(mapStateToProps, actionCreators)(hot(App));


You'll receive the error below if you forgot to import and configure redux-thunk to your project:

Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.


Here's an example configuration for redux-thunk:

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

import { reducersRoot } from './reducers-root';

import { IAllState } from './all-state';

import ReduxThunk from 'redux-thunk';

export function configureStore(): Store<IAllState>
{
    const middlewares = applyMiddleware(ReduxThunk);

    const composeEnhancers = (window as any)['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose;

    const composed = composeEnhancers(middlewares);

    return createStore(reducersRoot(), composed);
}

Another benefit of not using mapDispatchToProps is you can just pass the action creator directly to the action creators object, no need for the code to call the dispatch by itself. Making the code simple.

const actionCreators = {
    incrementCounter, 
    getLoggedUser
};

If needed be, it can be customized how an action creator is called:

const actionCreators = {
    incrementCounter: (n?: number) => incrementCounter(n! * 42),
    getLoggedUser
};

Friday, March 22, 2019

Error: Actions must be plain objects. Use custom middleware for async actions

If you got that error, it's likely that you forgot to import the redux-thunk and configure it accordingly similar to the code below:

import { createStore, Store } from 'redux';

import { reducersRoot } from './reducers-root';

import { IAllState } from './all-state';

export function configureStore(): Store<IAllState>
{
    const devTools: any = (window as any)['__REDUX_DEVTOOLS_EXTENSION__'];

    return createStore(reducersRoot(), devTools && devTools());
}


Solution:
import { applyMiddleware, compose, createStore, Store } from 'redux';

import { reducersRoot } from './reducers-root';

import { IAllState } from './all-state';

import ReduxThunk from 'redux-thunk';

export function configureStore(): Store<IAllState>
{
    const middlewares = applyMiddleware(ReduxThunk);

    const composeEnhancers = (window as any)['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose;

    const composed = composeEnhancers(middlewares);

    return createStore(reducersRoot(), composed);
}


Wednesday, March 20, 2019

Flattening redux state while maintaining navigability

Let's say you have this interface for mapStateToProps:

// file 1

export interface ILoggedUserState
{
    username: string;
    email: string;
    lastLogged: Date;
}

// file 2

export interface IAllState
{
    loggedUser: ILoggedUserState;
    chosenTheme: IChosenTheme;
    menu: IMenu;
}

// file 3

interface IPropsToUse
{
    loggedUser: ILoggedUserState;
    chosenTheme: IChosenTheme;
}

export const mapStateToProps = ({loggedUser, chosenTheme}: IAllState): IPropsToUse => ({
    loggedUser,
    chosenTheme
});




And you want to flatten or use only a few fields from ILoggedUserState, say we only want to use the username only:

interface IPropsToUse
{
    username: string;
    chosenTheme: IChosenTheme;
}

export const mapStateToProps = ({loggedUser: {username}, chosenTheme}: IAllState): IPropsToUse => ({
    username,
    chosenTheme
});


The downside of that code is it cannot convey anymore where the IPropsToUse's username property come from.

Here's a better way to convey that IPropsToUse's username come frome ILoggedUserState's username field.

interface IPropsToUse
{
    username: ILoggedUserState['username'];
    chosenTheme: IChosenTheme;
}

export const mapStateToProps = ({loggedUser: {username}, chosenTheme}: IAllState): IPropsToUse => ({
    username,
    chosenTheme
});


The syntax looks odd, but it works. With the code above, we can convey that IPropsToUse's username come from ILoggedUserState's username, and IPropsToUse's username also receives the type of ILoggedUserState's username.