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
.
- 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 withrxjs
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 aSAVE
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
, andFAILURE
, 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 asReadonly
to indicate that state is immutable. Particularly, nested objects should be marked asReadonly
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 .
Webmentions
does tamoxifen stop periods
does tamoxifen stop periods
purchase provigil in mexico
purchase provigil in mexico
ciprofloxacin vs levofloxacin
ciprofloxacin vs levofloxacin
lisinopril hctz 20 25mg tab
lisinopril hctz 20 25mg tab
does amoxicillin expire
does amoxicillin expire
stromectol order online
stromectol order online
viagra where to buy
viagra where to buy
ivermectin where to buy
ivermectin where to buy
indian brand names of tadalafil argentina
indian brand names of tadalafil argentina
purchase stromectol online
purchase stromectol online
levitra order pharmacy
levitra order pharmacy
pharmacy store
pharmacy store
pharmacy escrow viagra
pharmacy escrow viagra
is voltaren gel over counter
is voltaren gel over counter
actos reeditados
actos reeditados
how does protonix work
how does protonix work
robaxin for sciatica
robaxin for sciatica
what is tizanidine generic for
what is tizanidine generic for
tamsulosin price philippines
tamsulosin price philippines
alcohol and remeron
alcohol and remeron
best time of day to take abilify
best time of day to take abilify
ezetimibe category
ezetimibe category
what is ashwagandha root
what is ashwagandha root
citalopram reviews for anxiety
citalopram reviews for anxiety
flomax prostate pain
flomax prostate pain
is augmentin penicillin
is augmentin penicillin
neurontin patient assistance program
neurontin patient assistance program
can you drink while taking cephalexin
can you drink while taking cephalexin
how to discontinue duloxetine
how to discontinue duloxetine
escitalopram 10 mg price at walmart
escitalopram 10 mg price at walmart
metronidazole actinomyces
metronidazole actinomyces
gaining weight on rybelsus
gaining weight on rybelsus
lisinopril gallstones
lisinopril gallstones
lasix nevenwerking
lasix nevenwerking
metformin stillbirth
metformin stillbirth
can pregabalin and gabapentin be taken together
can pregabalin and gabapentin be taken together
valtrex ndc
valtrex ndc
tamoxifen noncompliance
tamoxifen noncompliance
sulfamethoxazole-trimethoprim for kidney infection
sulfamethoxazole-trimethoprim for kidney infection
flagyl tandläkare
flagyl tandläkare
cheap cialis pills men
cheap cialis pills men
maximpeptide tadalafil review
maximpeptide tadalafil review
buy viagra online united states
buy viagra online united states
sildenafil brand name india
sildenafil brand name india
female viagra over the counter
female viagra over the counter
how much is generic viagra in mexico
how much is generic viagra in mexico
research chemicals tadalafil purchase peptides
research chemicals tadalafil purchase peptides
generic sildenafil coupon
generic sildenafil coupon
purchase adipex from an online pharmacy
purchase adipex from an online pharmacy
where can i buy cialis over the counter
where can i buy cialis over the counter
tadalafil
tadalafil
[…] para que sirve foshlenn clindamicina[…]
para que sirve foshlenn clindamicina