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.

Sunday, July 1, 2018

401 on all browsers except Chrome Canary

Encountered a weird case where the program works on Chrome Canary, and then causes http 401 unauthorized on all other browsers (Firefox, Safari). I even installed a new browser (Opera) to see if the program is working there, but to no avail.

It turns out Canary includes the credentials (cookie) even it is not explicitly specified in fetch's options. To make fetch work on all browsers, add credentials: 'include' option explicitly.


await fetch('urlHere', {credentials: 'include'});


Saturday, June 2, 2018

Nameless interface with TypeScript

There are only two hard things in Computer Science: cache invalidation and naming things. -- PHil Karlton


Why give names to actions when your action and reducer can discriminate an action based on type (e.g., MyJobActionType) ?
export const enum MyJobActionType
{
    MY_JOB               = 'MY_JOB',

    MY_JOB_UI            = 'MY_JOB_UI',
    MY_JOB_DATA_FETCHING = 'MY_JOB_DATA_FETCHING',
    MY_JOB_DATA_FETCHED  = 'MY_JOB_DATA_FETCHED',

}

export type MyJobAction = IMyJobAction | IMyJobUIAction | IMyJobDataFetchingAction | IMyJobDataFetchedAction;

interface IMyJobAction extends Action
{
    type: MyJobActionType.MY_JOB;
    payload?: {
        jobId: 'new' | number;
    };
}

interface IMyJobUIAction extends Action
{
    type: MyJobActionType.MY_JOB_UI;
    ui: React.ComponentClass;
}

interface IMyJobDataFetchingAction extends Action
{
    type: MyJobActionType.MY_JOB_DATA_FETCHING;
}

interface IMyJobDataFetchedAction extends Action
{
    type: MyJobActionType.MY_JOB_DATA_FETCHED;
    data: IPagedDto<IPagedMyJobPostDto>;
}

Reducer:
export const myJobViewModelReducer = produce((
    draft: IMyJobViewModel = {
        view : null,
        model: {
            gridData: no.data
        }
    },
    action: MyJobAction
) =>


Action:
async function loadUI(dispatch: Dispatch<MyJobAction>)
{
    const component = (await import(/* webpackChunkName: 'myJob' */ './')).default;

    const uiAction: MyJobAction = {
        type: MyJobActionType.MY_JOB_UI,
        ui  : component
    };

    await dispatch(uiAction);
}


So don't name things then:
export type MyJobAction =
    {
        type: MyJobActionType.MY_JOB;
        payload?: {
            jobId: 'new' | number;
        };
    }
    |
    {
        type: MyJobActionType.MY_JOB_UI;
        ui: React.ComponentClass;
    }
    |
    {
        type: MyJobActionType.MY_JOB_DATA_FETCHING;
    }
    |
    {
        type: MyJobActionType.MY_JOB_DATA_FETCHED;
        data: IPagedDto<IPagedMyJobPostDto>;
    }
    ;

Are you worried someone might use the wrong payload (e.g., payload, ui, data) when dispatching an action? Don't worry, TypeScript is a smart language.




And just like that, TypeScript can infer that only the ui property is if a good discriminator is used, e.g., type: MyJobActionType.MY_JOB_UI




By the way, don't use the Dispatch type definition from redux, use the one from react-redux, it has more comprehensive type-checking. Upgraded to Redux 4, no more type definition problem.


So if you want TypeScript to type-check your direct object parameter to dispatch function, use react-redux's Dispatch type definition instead: Use Redux 4.0 instead, it comes with the correct type definition.



Finally, correct the properties of the object structure that matches type: MyJobActionType.MY_JOB_UI




Less interfaces, less names need to come up with.

Thursday, May 31, 2018

timestamptz is easier to adjust than timestamp

x=# create table z(id int primary key, _timestamp timestamp, _timestamptz timestamptz);
CREATE TABLE
x=# 
x=# insert into z(id, _timestamp, _timestamptz) values (1, '2018-05-04T17:37:00Z', '2018-05-04T17:37:00Z');
INSERT 0 1
x=# 
x=# set timezone to 'Asia/Manila';
SET
x=# select 
x-#   _timestamp, 
x-#  _timestamptz, 
x-#  _timestamp at time zone 'UTC' at time zone 'Asia/Tokyo' as "timestamp in Japan", 
x-#  _timestamptz at time zone 'Asia/Tokyo' "timestamp in Japan",
x-#  _timestamp at time zone 'UTC' at time zone 'Asia/Manila' "timestamptz in Philippines", 
x-#  _timestamptz at time zone 'Asia/Manila' as "timestamptz in Philippines"
x-# from z;
-[ RECORD 1 ]--------------+-----------------------
_timestamp                 | 2018-05-04 17:37:00
_timestamptz               | 2018-05-05 01:37:00+08
timestamp in Japan         | 2018-05-05 02:37:00
timestamp in Japan         | 2018-05-05 02:37:00
timestamptz in Philippines | 2018-05-05 01:37:00
timestamptz in Philippines | 2018-05-05 01:37:00

x=# 
x=# set timezone to 'Asia/Tokyo';
SET
x=# select 
x-#   _timestamp, 
x-#  _timestamptz, 
x-#  _timestamp at time zone 'UTC' at time zone 'Asia/Tokyo' as "timestamp in Japan", 
x-#  _timestamptz at time zone 'Asia/Tokyo' "timestamp in Japan",
x-#  _timestamp at time zone 'UTC' at time zone 'Asia/Manila' "timestamptz in Philippines", 
x-#  _timestamptz at time zone 'Asia/Manila' as "timestamptz in Philippines"
x-# from z;
-[ RECORD 1 ]--------------+-----------------------
_timestamp                 | 2018-05-04 17:37:00
_timestamptz               | 2018-05-05 02:37:00+09
timestamp in Japan         | 2018-05-05 02:37:00
timestamp in Japan         | 2018-05-05 02:37:00
timestamptz in Philippines | 2018-05-05 01:37:00
timestamptz in Philippines | 2018-05-05 01:37:00