Typescript with Redux, & Redux-Thunk Recipe

Kittiphat Srilomsak
5 min readOct 10, 2018

--

Typescript is the great tools for saving bugs and confusion when it comes to really large project. One may say that Types is a powerful tool of communication between developers. Anyway React is a javascript framework that can also be powered by Typescript.

At this point I assume that readers already have a basic knowledge of the 3 main components Typescripts, React, Redux.

The example

So let’s consider the simple login page. Using store name ‘session’.

Redux, the truth holder, should have their own code structured and placed in its own section. In my case I place them in store directory.

store
+- session
| +- actions.ts
| +- reducers.ts
+- index.ts

The structure described above is how each of my store would be define. index.ts collects all the store and create rootReducer. Of which we will come back and see this through later.

Now let’s examine each part of store. My simple store comprise of 2 parts. actions.ts and reducers.ts

Think of action as your function’s signature. It should be defined as interface. And everyone else should just either implement them and use them as signature described. This is where Typescript would save the day. The idea is that to create new action. You should create one interface to describe your action object. And with the help of Typescript’s type discriminator. You can union these actions later on and export it. To understand this. Let see our actions.ts.

// store/session/actions.ts
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux';
// Action Definitionexport interface SetAction {
type: 'SET'
accessToken: string
}
export interface SetFetcing {
type: 'SET_FETCHING'
isFetching: boolean
}
// Union Action Typesexport type Action = SetAction | SetFetcing// Action Creatorsexport const set = (accessToken: string): SetAction => {
return { type: 'SET', accessToken }
}
export const isFetching = (isFetching: boolean): SetFetcing => {
return { type: 'SET_FETCHING', isFetching }
}
export const login = (username: string, password: string): ThunkAction<Promise<void>, {}, {}, AnyAction> => {
// Invoke API
return async (dispatch: ThunkDispatch<{}, {}, AnyAction>): Promise<void> => {
return new Promise<void>((resolve) => {
dispatch(isFetching(true))
console.log('Login in progress')
setTimeout(() => {
dispatch(set('this_is_access_token'))
setTimeout(() => {
dispatch(isFetching(false))
console.log('Login done')
resolve()
}, 1000)
}, 3000)
})
}
}

Now this means your actions will be easily distinguish in your reducers implementation. And with modern IDE it will hint the type value automatically for you.

Notice that there is our Login action returns a promise (using async/await) instead of Action interface. Normally Redux cannot handle asynchronous calls out of the box. It needs help from their 3rd party middleware. In this case: Redux-Thunk. Normally, by the doc, redux’s dispatch method only received Action items which is object. Not a function. To provide a function call instead. You need to provide them with the middleware. And with the help of specific middleware then you will be allowed to invoke dispatch with functions instead. You will see that in action very soon. Now let see our reducer.

// store/session/reducers.tsimport { combineReducers } from 'redux'import { Action } from './actions'// States' definitionexport interface AccessToken {
isFetching: boolean
accessToken?: string
}
export interface State {
accessToken: AccessToken
}
const accessToken = (state: AccessToken = { isFetching: false }, action: Action): AccessToken => {
switch (action.type) {
case 'SET':
return { ...state, accessToken: action.accessToken }
case 'SET_FETCHING':
return { ...state, isFetching: action.isFetching }
}
}
export default combineReducers<State>({
accessToken
})

Normally the accessToken reducer method would complain that the action doesn’t have attribute accessToken but that’s not the case here. As types discrimination for Union type we defined earlier in our actions.ts. It automatically check the type when you use switch statement to handle action.type and tell compiler the correct interface in each case block.

Here it is also important that you would define the State interface here. So that the type information is clear. And you can combine them with other store when you combine them at root level.

Now lastly let’s see our store/index.ts for how can we combine the stores together.

// store/index.tsimport { createStore, combineReducers, applyMiddleware } from 'redux'
import session, { State as SessionState } from './session/reducers'
import thunk from 'redux-thunk'
export interface RootState {
session: SessionState
}
export default createStore(combineReducers<RootState>({
session
}), applyMiddleware(thunk))

At this point you got your store and they are fully typed.

Now how to use this store? — To use the store you will need to wrap the component you would want to use or the parent of such component with special component Provider so my whole app need this. I decided to put it around my App.tsx like so:

Now for any component that would like to use the store. You need to perform connect and provide a mapping functions between the Store’s state and Component’s props.

So let’s see our Login.tsx .

The key part of using typescript is to define types in different parts and combine them together. Namely each Component would need 2 types definition. (1) State and (2) Prop. With the help of Typescript that allow use to combine types together. We can easily define each part of types separately. As a result our props came from

interface State { ... } // Internal state for the component
interface StateProps { ... } // Props those being mapped from Store
interface DispatchProps { ... } // Dispatchable methods (invoke our store's actions)
interface OwnProps { ... } // Normal properties for the component
// combine them together
type Props = StateProps & DispatchProps & OwnProps
// Use them
class Login extends React.Component<Props, State> { ... }

Now that we done with the definitions. It is time for mapping the props with store. To do this we use redux’s connect method and provide 2 special functions that return object as a props to Component.

Map States to Props

This method will be invoked automatically so that it will update the properties that will be passed down to Component. This method has 2 arguments first is the root states and second is the Component’s own props (you may use them in order to compute/transform the states.

// Login.tsx - mapStateToPropsconst mapStateToProps = (states: RootState, ownProps: OwnProps): StateProps => {
return {
accessToken: states.session.accessToken
}
}

Map Dispatch to Props

With the same fashion this method is to map the actions and provide it back as a properties of the component.

This method provide a callable dispatch method (this is actually store.dispatch method. However store.dispatch under the modification from middleware it can be changed to the last middleware provided. So I declare its type using ThnnkDispatch instead so that our dispatch method would be able to use function that return callable instead of Action interface.

// Login.tsx - mapDispatchToPropsconst mapDispatchToProps = (dispatch: ThunkDispatch<{}, {}, any>, ownProps: OwnProps): DispatchProps => {
return {
login: async (username, password) => {
await dispatch(login(username, password))
console.log('Login completed [UI]')
}
}
}

Finally you connect all of these together like so

export default connect<StateProps, DispatchProps, OwnProps, RootState>(mapStateToProps, mapDispatchToProps)(Login)

That’s it! Once you are done. there will be properties that holds callable methods as you can see in my Login button.

I’ve created the repository that can be cloned and tried here. https://github.com/peatiscoding/mex-redux-redux-thunk

Note: Part of my post was based on: https://medium.com/knerd/typescript-tips-series-proper-typing-of-react-redux-connected-components-eda058b6727d

--

--