Writing a simple React-Redux app with Typescript — Part 1

Zac Tolley
Scropt

--

if javascript frontend development is still the ‘Wild West’ compared to frameworks like Django or Spring then React and Redux is the town full of crazy stuck out in the middle of nowhere with no sheriff.

I’ve used React and Redux on and off for little projects, each time going through the tutorial, getting a basic simple app up and then having to put it on hold so I could do some ‘Real work’. 6 months later I’d come back to Redux and learn it all over again. The last time I did this I wanted to give Typescript a go as everyone talks about how much easier it makes development. I only came across a couple of articles that gave examples how to use it and ‘Easy’ is not the word that sprung to mind. There was more ‘Type’ code than real code. I could see no benefit in adding a ton of bloatware into a project that would end up making it harder to maintain, certainly not easier.

Last week I had a need to learn React and Redux again and the same thing bugged me, it’s a pain to have to keep the model in your head and it would be so much nicer if I could write containers/components and know that fields and properties I could use instead of guess or hope the editor somehow would figure it out. I also kind of miss the old days of Java, but don’t tell anyone I said that ok?

I wondered if Typescript had improved or if the code out there had matured. After reading around it seems that more people have tried Redux and Typescript and the subject has matured a little. I found some articles and videos that look easier than before but it’s still a bit all over the place. So in this world of differing opinions and tutorials on how to use TypeScript and Redux I’m adding another.

As part of this exercise I also implemented the same project in Flow to see how it compared and was surprised how almost identical the code was though there are some important differences and in my case I did find a clear indicator which worked best for me.

So the aim of this series of articles is to create a simple project to list contacts, show one of them and then let the user edit it. Nothing fancy, the aim is to demonstrate the use of Typescript with Redux and the pro’s and cons.

Part 1 will walk you through setting up a React Typescript project and making a start on some typed Redux code.

So to start we need the simplest app. If you haven’t already (and I know you have) install Node. I’m using the current LTS 8.11.3. Oh BTW, if you use Windows (I’m one of the few that do) there’s a couple of NVM tools around, I use nvm-windows.

Once you have node installed go ahead and install ‘Yarn’ and ‘Create React App’

npm install -g yarn create-react-app

Create React App is a painless way to get a react application up and runnign in seconds without fighting with Webpack. You can also use alternative templates and one of these is create-react-app-typescript

So to create a basic react app that uses Typescript simply

create-react-app contacts --scripts-version=react-scripts-ts
cd contacts/
yarn start

Before we start building any components or state stuff, lets define something. The application will manage contacts, so lets define what a contact looks like.

Typescript borrows idea from other languages like Java in that it supports classes and interfaces. A Class defines the properties of a ‘thing’ and contains functions/methods you can call to do something with instances of that ‘thing’, i.e. a Dog class would have a property ‘colour’ and a method ‘bark’. Typescript supports ES6 classes and enhances them further with things such as public/private accessors. In addition to ES6 classes Typescript supports Interfaces. An Interface describes what a thing will look like, in the case of the Dog class the interface will say it has a colour and a method called ‘bark’ but wont actually store the colour or contain the logic for the bark method. If something passes an object that it says implements the ‘Dog’ interface then you know that it will have a colour a you can call ‘bark’ on it. This is important when dealing with inheritance or can be used to assume the shape of an object without having to define how it work. They are like contracts that promise what a thing will look like. In our case we will be passing Contact objects around, so we describe what a Contact looks like.

# src/models/IContact.tsexport default interface IContact {
id?: string
name: string
email?: string
}

The interface has ‘I’ at the front because the typescript linter says it’s good practice. Later we may want to introduce a Contact class and we could say that it conforms to the IContact interface to make sure it behaves as expected. ‘id’ is an optional string, as new contacts won’t have one. Email is also optional and name is a required.

Redux has the idea of a store, which represents state. Some simple applications may have a single store with a single reducer and a few actions but in this case we are going to create a root store and build it by combining others. The first store we will create will focus on contacts and will contain a list of contacts and a currently selected contact when we are looking at a detail or an edit screen.

Before we write some code, let’s add Redux to the project.

yarn add redux react-redux 
yarn add --dev @types/react-redux

Notice the extra install into our dev dependencies. If you look at your package.json you’ll see a list of dependencies like this. This dependency contains type declarations for react-redux. It helps Typescript know what react-redux looks like and helps it process the types for code that use it.

We are going to create an action that fetches a list of contacts from a server. In a real app we’d call the server, but for now lets make it fake.

# src/api/contacts.tsimport IContact from '../models/IContact'const contact: IContact = {
id: '1',
name: 'Fred',
}
export async function fetchContacts(
page: number = 1,
limit: number = 10
)
: Promise<IContact[]> {
return [contact]
}
export async function fetchContact(id: string)
: Promise<IContact> {
return contact
}

Note how we are already using type to describe the parameter types for function calls and the type of result that will be returned. Look how easy promises can be typed.

As this is an asynchronous action we’ll use ‘redux-thunk’ to allow us to make async calls and dispatch events with the result.

yarn add redux-thunk

# src/store/contacts/actions.tsimport { fetchContacts } from '../../api/contacts'
import IContact from '../../models/IContact'
export function getContacts() {
return (dispatch) => {
fetchContacts().then((contacts: IContact[]) => {
dispatch(createLoadContactsAction(contacts))
})
}
}

Your editor may have a ton of wavy lines, don’t worry. There is some type information we need to add to this but we’ll get to that in a moment. For now all we’ve done is create a thunk action that calls ‘fetchContacts’ and the makes a call to an action creator and dispatches the event.

The thunk calls an action creator, this is expected to create an action that has a type and a payload. To help us later, and avoid typos, we should define our actions in a constant or something that can be shared. It seems like a good idea to use enums to list the possible action types.

# src/store/contacts/actions.tsexport enum actionTypes {
LOAD_CONTACTS = 'LOAD_CONTACTS',
LOAD_CONTACT = 'LOAD_CONTACT',
CLEAR_CURRENT_CONTACT = 'CLEAR_CURRENT_CONTACT',
}

Enums make it easy to define a set of constants to use through your code but, as pointed out by a sharp eyed reader, the default behaviour for Typescript is to simply convert them into Integers, this means your actions are not guaranteed to be unique. As actions need to be unique you need to specify the value for each enum constant

Now the action creator. Action creators are incredibly simple, and you could put them in your thunk actions, but splitting them out makes it easier to type check them. They may not be doing much but it never hurts to ‘measure twice, cut once’. Again the syntax uses a traditional function style here though some may use arrow functions. I left it like this to be consistent and easier to read with type information.

# src/store/contacts/actions.tsfunction createLoadContactsAction(contacts: IContact[]) {
return {
payload: contacts,
type: actionTypes.LOAD_CONTACTS,
}
}

We are already starting to benefit from type checking, this function expects to be sent an array of objects that conform to the IContact interface.

Almost there: we get the contacts, create an action with the contacts in it and dispatch it. Now the icing on the cake, some more type checking info to get rid of the red lines and make sure the data we are passing around is correct.

First those actions. The reason we are using Typescript is that we want to make sure we don’t make silly mistakes. In the case of actions we want to make sure that the actions are created with the correct combination of type and payload. In the case of getting the contact list we want to ensure that the LOAD_CONTACTS action always has a contact array as it’s payload.

# src/store/contacts/actions.tsinterface ILoadContactsAction {
type: typeof actionTypes.LOAD_CONTACTS
payload: IContact[]
}
function createLoadContactsAction(contacts: IContact[])
:ILoadContactsAction {
return {
payload: contacts,
type: actionTypes.LOAD_CONTACTS,
}
}

Now we are checking that the action returned conforms to the ILoadContactsAction interface. If you are using an editor like Visual Studio Code then your wavy line in that function should have disappeared. For fun, replace the ‘payload: contacts’ line with ‘payload: 10’, you should get a typescript error. Yay!

Now the a weird bit. The thunk action needs type information too. We need to tell it that the dispatch function it receives as a parameter can contain different types of actions, and also tell it what a Thunk action is. I actually found this in an article on the FlowType website, but it works for Typescript too, the two are almost identical sometimes.

# src/store/contacts/actions.tstype PromiseAction = Promise<ILoadContactsAction>
type ThunkAction = (dispatch: Dispatch) => any
type Dispatch = (action:
ILoadContactsAction |
ThunkAction |
PromiseAction
) => any

Redux-Thunk can provide a type definition for ThunkAction, which also include the other parameters available, not shown here, such as getState. At the time of writing all the examples I found that use this do not match the latest version of Redux-Thunk so for now, this will do. Right now we are just using the ILoadContactsAction but in a later part we will add more to this piece of code.

Update your getContacts now

# src/store/contacts/actions.tsexport function getContacts(): ThunkAction {
return (dispatch: Dispatch) => {
fetchContacts()
.then((contacts: IContact[]) => {
dispatch(createLoadContactsAction(contacts))
})
}
}

So now the ‘getContacts’ action indicates it returns a ‘ThunkAction’ which is a function that accepts a parameter of ‘Dispatch’ type. The dispatch function passed into the thunk has a parameter that must be an action.

This application needs 2 more actions, so lets do them before we move onto the reducer and show another bit of Typescript magic.

In addition to getting a collection of contacts we need to be able to load the details for a single contact and reset the selected contact back to null. We already created the action types when we specified our ‘actionTypes’ enum, but we need a thunk action and a couple of action creators

# src/stopre/contacts/actions.tsinterface ILoadContactAction {
type: typeof actionTypes.LOAD_CONTACT
payload: IContact
}
interface IClearContactAction {
type: typeof actionTypes.CLEAR_CURRENT_CONTACT
}
function createLoadContactAction(contact: IContact)
:ILoadContactAction {
return {
payload: contact,
type: actionTypes.LOAD_CONTACT,
}
}
export function clearCurrentContact()
:IClearContactAction {
return {
type: actionTypes.CLEAR_CURRENT_CONTACT,
}
}
export function getContact(id: string): ThunkAction {
return (dispatch: Dispatch) => {
fetchContact().then((contact: IContact) => {
dispatch(createLoadContactAction(contact))
})
}
}

No surprises here, it all looks pretty straightfoward. There is one last thing, and it just makes a little easier. In Typescript you can have a ‘type’ which can be one of a number of different types, we used this earlier for dispatch. To make things neater you can create an ‘Action’ type which says it is always one of a list of different types or interfaces. So add this and change your function signatures for the action creators.

export type Action =
| ILoadContactsAction
| ILoadContactAction
| IClearContactAction
function createLoadContactsAction(contact: IContact):Action {function createLoadContactAction(contact: IContact):Action {export function clearCurrentContact():Action {

This makes it a little easier when importing action types into the reducer. Now here’s the magic bit. Go ahead and change the payload value for one of those action creators, it shows an error. The Typescript compiler is smart enough to still know which of the Interfaces applies to the return value based on the value of the ‘type’ property, it still knows that when you load a contact it wants a single IContact. The ‘Action’ type is exported as it will be used by the reducer and the clearCurrentContact action is exported as it will be used directly, much like the thunkActions. There’s no point exporting the other action creators as we don’t access them outside the file and we wont need to test them now that Typescript is ensuring they are correct. We also tidy up the other types in the file to use ‘Action’ to keep the code neat.

If you have been following along you should have:

# src/models/IContact.tsexport default interface IContact {
id?: string
name: string
email?: string
}
# src/api/contacts.tsimport IContact from '../models/IContact'const contact: IContact = { id: '1', name: 'Fred' }export function getContacts(): Promise<IContact[]> {
return new Promise(resolve => {
resolve([contact])
})
}
export function getContact(id: string): Promise<IContact> {
return new Promise(resolve => {
resolve(contact)
})
}
# src/store/contacts/actions.tsimport { fetchContact, fetchContacts } from '../../api/contacts'
import IContact from '../../models/IContact'
// Type information to define strictly typed actions
export enum actionTypes {
LOAD_CONTACTS = 'LOAD_CONTACTS',
LOAD_CONTACT = 'LOAD_CONTACT',
CLEAR_CURRENT_CONTACT = 'CLEAR_CURRENT_CONTACT',
}
interface ILoadContactsAction {
type: typeof actionTypes.LOAD_CONTACTS
payload: IContact[]
}
interface ILoadContactAction {
type: typeof actionTypes.LOAD_CONTACT
payload: IContact
}
interface IClearContactAction {
type: typeof actionTypes.CLEAR_CURRENT_CONTACT
}
export type Action =
| ILoadContactsAction
| ILoadContactAction
| IClearContactAction
type PromiseAction = Promise<Action>
type ThunkAction = (dispatch: Dispatch) => any
type Dispatch = (action: Action | ThunkAction | PromiseAction) => any
// Action creators
function createLoadContactsAction(contacts: IContact[]): Action {
return {
payload: contacts,
type: actionTypes.LOAD_CONTACTS,
}
}
function createLoadContactAction(contact: IContact): Action {
return {
payload: contact,
type: actionTypes.LOAD_CONTACT,
}
}
export function clearCurrentContact(): Action {
return {
type: actionTypes.CLEAR_CURRENT_CONTACT,
}
}
// Thunk Actions
export function getContacts(): ThunkAction {
return (dispatch: Dispatch) => {
fetchContacts().then((contacts: IContact[]) => {
dispatch(createLoadContactsAction(contacts))
})
}
}
export function getContact(id: string): ThunkAction {
return (dispatch: Dispatch) => {
fetchContact(id).then((contact: IContact) => {
dispatch(createLoadContactAction(contact))
})
}
}

I will say that this point that there is a fair amount of type code already, but there isn’t much more to add. The type code we have may appear to duplicate some of the application, and indeed when you use flow you can ask it to calculate some of those types. I find them easy to read, they stop me making silly mistakes and they make me think more.

In the example above if I was to go to the action creator and change the payload value or the type value Typescript would tell me there was a type error. Later in the components and containers we’ll get the benefits of type checking and smarter property suggestions, rather than the editor guessing.

The next step, in part 2, will be create a reducer handle the events and state changes then wire up the stores and middle-ware in preparation for part working on containers and components.

The code for part 1 can be found in the github repo that accompanies this article.

I’m still relatively new to React/Redux so there many be silly stuff I’ve missed or ideas you have and I welcome your feedback.

--

--