Typescript with Redux, & Redux-Thunk Recipe
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