in Front End

Typing React (3) Redux

In previous articles we have already explained how to use TypeScript in regular React and with Material-UI. In this article I will show the most important and difficult part: redux.

Previous articles can be found here:

The packages used in this articles are:

$ npm install --save redux react-redux typesafe-actions \
              rxjs redux-observable lodash reselect \
              @types/react-redux

The common and recommended way of using redux is with the combination of redux + typesafe-actions + redux-observable. The configuration is a bit complicated so I will explain piece by piece.


Basic Concepts

In this tutorial I will use the following directory structure:

src/
  |- store
  |    |- actions
  |    |- reducers
  |    |- epics
  |    `- selectors
  `- services

The directory names should be pretty self-explained.

The following figure shows the overall concept of redux + typesafe-action + redux-observable.

Concept of Redux store
  • The core part of the redux store is an action stream. An action is simply a piece of data with two fields: { type: string; payload: any }. As it is a stream, it can be represented with rxjs Observable.
  • Actions are generated in two ways: dispatched by a Component, or generated by an Epic.
  • A Component can dispatch an action at any time. Usually this is triggered by an event, e.g. dispatch a LIST event after page is loaded, or dispatch a SAVE event after user clicks a button.
  • An Epic is an object that watches the action stream. When a specific action appears in the stream, the epic will do something (usually a side effect) and then push zero or more actions (usually one) back to the stream. Here the word “side effect” simply means something that depends on something outside the store, e.g. an API call, a DOM operation, or even printing a log message.
  • A reducer watches the action stream and transit the state as necessary.

This seems complicated, so it would benefit to explain by example. Suppose we want to retrieve all the Todo items from API:

  • The Component dispatches an action with type TODO:LIST:REQUEST.
  • The Epic sees this action and triggers an API call. After the API call returns, it extracts the Todo list from response, assembles an TODO:LIST:SUCCESS action with the Todo list data, then push this action back to the stream.
  • The reducer receives TODO:LIST:SUCCESS action and extracts the Todo list data from it, then update global state with the Todo list.

Actions

Let’s start with the actions. There are two categories:

  • Standard action, simply an action;
  • Asynchronized action, consists of three actions: REQUEST, SUCCESS, and FAILURE, and is used for asynchronized calls.
import { createAsyncAction, createStandardAction } from 'typesafe-actions';
import { Todo } from 'Models';

// Standard action
export const setNote = createStandardAction('NOTE:SET_NOTE')<string>();

// Asynchronized actions
export const listTodo = createAsyncAction(
  'TODO:LIST:REQUEST',
  'TODO:LIST:SUCCESS',
  'TODO:LIST:FAILURE',
)<void, Todo[], Error>();

The type parameters after the function call (e.g. <string>, and <void, Todo[], Error>) are the payload types of the actions.

Reducers

The following code example shows a normalized store consists of two major fields: byId and allIds. Several points covered by this code are:

  • You need to declare a state type TodoState to define the shape of the state. Note all the objects should be marked as Readonly to indicate that state is immutable. Particularly, nested objects should be marked as Readonly as well.
  • Use TodoState['byId'] to access the attribute type.
  • RootAction is an aggregated type which we will explain later. Now all you need to know is that it represents all possible actions.
import _ from 'lodash';
import { getType } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Todo } from 'Models';
import { listTodo } from '../actions/todo';
import { RootAction } from 'StoreTypes';

export type TodoState = Readonly<{
  byId: Readonly<{ [key: number]: Todo }>;
  allIds: number[];
  loading: boolean;
}>;

const initialState: TodoState = {
  byId: {},
  allIds: [],
  loading: false,
};

const byId = (state: TodoState['byId'] = initialState.byId, action: RootAction) => {
  switch (action.type) {
    case getType(listTodo.success):
      return _.keyBy(action.payload, 'id');
    default:
      return state;
  }
};

const allIds = (state: TodoState['allIds'] = initialState.allIds, action: RootAction) => {
  switch (action.type) {
    case getType(listTodo.success):
      return _.map(action.payload, 'id');
    default:
      return state;
  }
};

const loading = (
  state: TodoState['loading'] = initialState.loading,
  action: RootAction,
) => {
  switch (action.type) {
    case getType(listTodo.request):
      return true;
    case getType(listTodo.success):
    case getType(listTodo.failure):
      return false;
    default:
      return state;
  }
};

export default combineReducers({ byId, allIds, loading });

Epics

Epics are functions that watch the action stream and do something when special action appears. Usually, it is an actions$.pipe() call with a sequence of oeprators.The first operator is usually a filter(isOfType(getType(action))) to filter out the action we interested in. And the operator sequence should eventually return zero or more actions, which will be pushed back to the action stream.

export const ListTodoEpic: RootEpic = (actions$, store, { todos }) =>
  actions$.pipe(
    filter(isOfType(getType(listTodo.request))),
    mergeMap(action =>
      todos.listTodos$().pipe(map(listTodo.success)),
    catchError(err => of(listTodo.failure(err))),
  );

Be careful that an Epic should NEVER return the action that it interested in! This will create an infinity loop. For example:

// DON'T DO THIS!
export const InfinityLoopEpic: RootEpic = (actions$, store, { todos }) =>
  actions$.pipe(
    filter(isOfType(getType(listTodo.request))),
    mergeMap(action => action),
  );

Types

To make typing easier, we can declare some global types. This is done by adding an index.ts to reducers, actions and epics directories, and a types.d.ts to declare the types.

// actions/index.ts
import * as TodoActions from './todo';

export default {
  todos: TodoActions,
};
// reducers/index.ts
import todos from './todo';
import { combineReducers } from 'redux';

export default combineReducers({
  todos,
});
// epics/index.ts
import { combineEpics } from 'redux-observable';
import * as todoEpic from './todo';

export default combineEpics(
  ...Object.values(todoEpic),
);

Note that combineEpics and combineReducers take different parameters. combineEpics takes a list of single epics (that’s why we need to destruct imported object values), while combineReducers takes a tree structure.

The code below shows how to define the root types.

// store/types.d.ts
declare module 'StoreTypes' {
  import { StateType, ActionType } from 'typesafe-actions';
  import { Services } from 'ServiceTypes';
  import { Epic } from 'redux-observable';

  export type Store = StateType<typeof import('./index').default>;
  export type RootAction = ActionType<typeof import('./actions').default>;
  export type RootState = StateType<ReturnType<typeof import('./reducers').default>>;
  export type RootEpic = Epic<RootAction, RootAction, RootState, Services>;
}

Create Store

Now reducers and epics are ready, we can write code to create the store:

import { applyMiddleware, createStore, compose } from 'redux';
import { createEpicMiddleware } from 'redux-observable';

import { RootAction, RootState } from 'StoreTypes';
import { Services } from 'ServiceTypes';

import rootReducer from './reducers';
import rootEpic from './epics';
import services from '../services';

export const epicMiddleware = createEpicMiddleware<
  RootAction,
  RootAction,
  RootState,
  Services
>({ dependencies: services });

export const composeEnhancers =
  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// configure middlewares
const middlewares = [epicMiddleware];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// rehydrate state on app start
const initialState = {};

// create store
const store = createStore(rootReducer, initialState, enhancer);

epicMiddleware.run(rootEpic);

// export store singleton instance
export default store;

Selectors

Typing of selectors is pretty straightforward.

import { createSelector } from 'reselect';
import { RootState } from 'StoreTypes';

export const selectTodoEntities = (state: RootState) => state.todos.byId;
export const selectTodoAllIds = (state: RootState) => state.todos.allIds;
export const selectTodoLoading = (state: RootState) => state.todos.loading;

export const selectTodoList = createSelector(
  [selectTodoEntities, selectTodoAllIds],
  (entities, allIds) => allIds.map(id => entities[id]),
);

Component

The last part is to map the store to the component. Since we need to access the attributes mapped by mapStateToProps and mapDispatchToProps, we need these attributes defined in the IProps type. So we can define IProps like this:

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { RootState } from 'StoreTypes';
import { selectTodoList } from '../store/selectors/todo';
import { Dispatch } from 'redux';
import { listTodo } from '../store/actions/todo';
import TodoItem from './TodoItem';

const mapStateToProps = (state: RootState) => ({
  todos: selectTodoList(state),
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  listTodo: () => dispatch(listTodo.request()),
});

export interface IProps
  extends ReturnType<typeof mapStateToProps>,
    ReturnType<typeof mapDispatchToProps> {}

const TodoList = (props: IProps) => {
  const { todos, listTodo } = props;

  useEffect(() => {
    listTodo();
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <TodoItem todo={todo} />
      ))}
    </div>
  );
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

That’s all for typing redux. Thanks for reading!


Originally posted at https://itnext.io/typing-react-3-redux-84e73e41db7f .

Write a Comment

Comment

Webmentions

  • mein-tierschild.de

    … [Trackback]

    […] There you will find 21399 more Information to that Topic: charlee.li/typing-react-3-redux/ […]

  • acompanhantes de luxo sp

    … [Trackback]

    […] There you can find 4233 more Information to that Topic: charlee.li/typing-react-3-redux/ […]

  • thuốc Vitara

    … [Trackback]

    […] Find More Info here on that Topic: charlee.li/typing-react-3-redux/ […]

  • linh tiên song đằng tố

    … [Trackback]

    […] Read More to that Topic: charlee.li/typing-react-3-redux/ […]

  • eBay music promo code

    … [Trackback]

    […] Information on that Topic: charlee.li/typing-react-3-redux/ […]

  • Darknet Linkverzeichnis

    … [Trackback]

    […] Information on that Topic: charlee.li/typing-react-3-redux/ […]

  • W88casino

    … [Trackback]

    […] There you can find 56626 additional Info on that Topic: charlee.li/typing-react-3-redux/ […]

  • W88 Casino

    … [Trackback]

    […] Read More to that Topic: charlee.li/typing-react-3-redux/ […]

  • W88casino

    … [Trackback]

    […] Find More Info here to that Topic: charlee.li/typing-react-3-redux/ […]

  • W88.tips

    … [Trackback]

    […] Here you will find 67941 additional Information to that Topic: charlee.li/typing-react-3-redux/ […]

  • W88casino

    … [Trackback]

    […] Find More on on that Topic: charlee.li/typing-react-3-redux/ […]

  • W88 Poker

    … [Trackback]

    […] Find More Information here on that Topic: charlee.li/typing-react-3-redux/ […]

  • W88casino

    … [Trackback]

    […] Find More here on that Topic: charlee.li/typing-react-3-redux/ […]

  • replica shoes

    … [Trackback]

    […] Find More on that Topic: charlee.li/typing-react-3-redux/ […]

  • anti screenshot

    … [Trackback]

    […] There you can find 71024 additional Information to that Topic: charlee.li/typing-react-3-redux/ […]

  • Buy Muha Meds Carts

    … [Trackback]

    […] Find More on to that Topic: charlee.li/typing-react-3-redux/ […]

  • where to purchase social shares

    where to purchase social shares

    oqdwswnpb pajgs mkabnxw rjmj msdfcluytvlrtis

  • Krypto tauschen

    … [Trackback]

    […] Read More on that Topic: charlee.li/typing-react-3-redux/ […]

  • Speedpaste kaufen

    … [Trackback]

    […] Find More Info here to that Topic: charlee.li/typing-react-3-redux/ […]

  • gui hang di my

    … [Trackback]

    […] Info to that Topic: charlee.li/typing-react-3-redux/ […]

  • dumps for sale

    … [Trackback]

    […] Find More on on that Topic: charlee.li/typing-react-3-redux/ […]

  • dich vu marketing

    … [Trackback]

    […] Find More Info here on that Topic: charlee.li/typing-react-3-redux/ […]

  • dich vu seo

    … [Trackback]

    […] Here you can find 86037 additional Info on that Topic: charlee.li/typing-react-3-redux/ […]

  • Dịch vụ marketing online

    … [Trackback]

    […] Find More Information here to that Topic: charlee.li/typing-react-3-redux/ […]

  • click here

    … [Trackback]

    […] Info to that Topic: charlee.li/typing-react-3-redux/ […]

  • https://hotmail.app.br

    … [Trackback]

    […] Here you can find 36011 additional Info to that Topic: charlee.li/typing-react-3-redux/ […]

  • Peter Comisar Disgraced Ex Goldman Sachs Banker Sued By Scooter Braun For Fraud

    … [Trackback]

    […] There you will find 76582 more Information on that Topic: charlee.li/typing-react-3-redux/ […]

  • gửi hàng đi mỹ tại bình dương

    … [Trackback]

    […] Read More Information here to that Topic: charlee.li/typing-react-3-redux/ […]

  • gui hang di my tai dong nai

    … [Trackback]

    […] Information on that Topic: charlee.li/typing-react-3-redux/ […]

  • gui hang di my tai hcm

    … [Trackback]

    […] Info on that Topic: charlee.li/typing-react-3-redux/ […]

  • gửi hàng đi mỹ

    … [Trackback]

    […] Info to that Topic: charlee.li/typing-react-3-redux/ […]

  • gửi hàng dhl

    … [Trackback]

    […] Read More on on that Topic: charlee.li/typing-react-3-redux/ […]

  • gửi hàng đi canada

    … [Trackback]

    […] Find More on that Topic: charlee.li/typing-react-3-redux/ […]

  • chuyen phat nhanh dhl

    … [Trackback]

    […] Read More on on that Topic: charlee.li/typing-react-3-redux/ […]

  • máy lọc nước đại thành

    … [Trackback]

    […] Info on that Topic: charlee.li/typing-react-3-redux/ […]

  • bồn nhựa

    … [Trackback]

    […] There you can find 56528 more Info to that Topic: charlee.li/typing-react-3-redux/ […]

  • cvv shop

    … [Trackback]

    […] Read More Information here on that Topic: charlee.li/typing-react-3-redux/ […]

  • sexybaccarat

    … [Trackback]

    […] Find More here on that Topic: charlee.li/typing-react-3-redux/ […]

  • mortgage

    … [Trackback]

    […] There you will find 70986 additional Information on that Topic: charlee.li/typing-react-3-redux/ […]

  • My Homepage

    … [Trackback]

    […] Informations on that Topic: charlee.li/typing-react-3-redux/ […]