Integrating TypeScript and redux-thunk

typescript and react

Note: this is the fifth entry in a short series about bolstering Redux applications with TypeScript. Part one introduced a simple counter application, which we then dressed up with a simple React UI and API interactions. Read on!

When last we dealt with the outside world, a redux middleware was helping insulate our counter application from the messy details of API interaction. This has the advantage of letting us consider business logic independent of the transport layer; it was also convenient for illustration. But it was also rather verbose, as the middleware added an additional step of indirection on top of redux’s existing data flow.

We can do better, and a community-supported middleware–redux-thunk–can help. As its name suggests, redux-thunk allows us to dispatch actions defined as functions (the thunk). In our counter application, this will allow us to collapse the API interactions currently contained in our middleware into constituent action creators.

If you’re just looking to see the refactored application in action, check out the example project over on github. For everyone else, let’s get started.

The beginning

Our middleware set out to solve the challenge of separating our mostly-pure redux application from the vagaries of network I/O. API requests were initiated in response to specific actions (X_REQUEST), and their success or failure expressed in terms of additional actions dispatched to the application. In code, it looked like this:

export const apiMiddleware = ({ dispatch }: redux.MiddlewareAPI<any>) =>
  (next: redux.Dispatch<any>) =>
    (action: Action) => {
      switch (action.type) {
        case 'SAVE_COUNT_REQUEST':
          api.save(action.request)
            .then(() => dispatch(saveCount.success({}, action.request)))
            .catch((e) => dispatch(saveCount.error(e, action.request)))
          break

        case 'LOAD_COUNT_REQUEST':
          api.load()
            .then(({ value }) => dispatch(loadCount.success({ value }, action.request)))
            .catch((e) => dispatch(loadCount.error(e, action.request)))
          break
      }

      return next(action)
    }

This allowed us to define logic on top of our actions without imposing side-effects on either the redux store or individual react components.

But we could achieve a very similar result if our actions were wrapped in functions that had could dispatch further actions. In other words, we’d love to be able to define an action-creator, saveCount, that will wrap the entire API interaction:

type Q<T> = { request: T }
type S<T> = { response: T }
type E = { error: Error }

type QValue = Q<{ value: number }>

export type Action =
  ({ type: 'SAVE_COUNT_REQUEST' } & QValue)
| ({ type: 'SAVE_COUNT_SUCCESS' } & QValue & S<{}>)
| ({ type: 'SAVE_COUNT_ERROR'   } & QValue & E)
// ...

export type ApiActionGroup<_Q, _S> = {
  request: (q?: _Q)         => Action & Q<_Q>
  success: (s: _S, q?: _Q)  => Action & Q<_Q> & S<_S>
  error: (e: Error, q?: _Q) => Action & Q<_Q> & E
}

const _saveCount: ApiActionGroup<{ value: number }, {}> = {
  request: (request) =>
    ({ type: 'SAVE_COUNT_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'SAVE_COUNT_SUCCESS', request, response }),
  error: (error, request) =>
    ({ type: 'SAVE_COUNT_ERROR',   request, error }),
}

export function saveCount(request: { value: number }) {
  return (dispatch: redux.Dispatch<Store.All>) => {
    dispatch(_saveCount.request(request))
    api.save(request)
      .then((response) => dispatch(_saveCount.success(response, request)))
      .catch((e: Error) => dispatch(_saveCount.error(e, request)))
  }
}

Inside connected components, we would dispatch this action just as before.

// ...
onSaveClick: (value: number) =>
  dispatch(saveCount({ value })),

Not only will redux-thunk let us do this, but it will make it almost trivial. We just need to install it.

First, grab it from NPM:

$ npm install --save redux-thunk

Next, import 'redux-thunk' and include it in the middleware stack of the main redux store:

import thunk from 'redux-thunk'

import {
  reducers,
  Store,
} from './reducers'

// ...

let store: redux.Store<Store.All> = redux.createStore(
  reducers,
  {} as Store.All,
  redux.applyMiddleware(thunk),
)

Could it be that easy? It is!

Fire up the counter, and voilà: we can now dispatch the saveCount action creator like any other action.

Refactoring

Breaking the middleware into its constituent (and easily-tested) parts is a win, but we’re not done yet. As written, saveCount depends on both a rather unwieldy type definition, and ApiActionGroup, in addition to the action creator itself. There’s not much to do about the definitions, but the creator at least can be generalized in terms of the corresponding ApiActionGroup and API request.

type apiFunc<Q, S> = (q: Q) => Promise<S>

function actionCreator<Q, S>(x: ApiActionGroup<Q, S>, go: apiFunc<Q, S>) {
  return (request: Q) => (dispatch: redux.Dispatch<Store.All>) => {
    dispatch(x.request(request))
    go(request)
      .then((response) => dispatch(x.success(response, request)))
      .catch((e: Error) => dispatch(x.error(e, request)))
  }
}

With actionCreator added to our action module, it’s trivial to prepare existing ApiActionGroups for export:

export const saveCount = actionCreator(_saveCount, api.save)

Conclusion

redux-thunk has tidied things up nicely, while the original middleware continues working as before. That means that we could phase actionCreator in gradually without affecting the overall behavior of the project: not a big concern for our toy counter, but a boon for integration with any larger project.

The completed counter project is available for reference on github, build, code, tests, and all. And I’m looking forward to your suggestions, experiences, and feedback over on twitter.



Testing TypeScript with Jest

typescript and react

Note: this is the fourth entry in a short series about creating React/Redux applications with TypeScript. Part one introduced a simple counter application, which we then dressed up with a simple React UI and API interactions. Read on!

Followers of this blog won’t need to be convinced of the value of a well-maintained test suite. And lucky for us, Jest makes it nearly painless to test React applications–even with TypeScript in the mix. Let’s look at the layers involved in developing clean, test-secured code.

Linting

In vanilla JavaScript, linting validates syntax as a first line of defense against runtime errors. In TypeScript, static type-checking serves a similar purpose, though with a more sophisticated understanding of the raw syntax’s context. But linters have a secondary purpose: ensuring maintainability. Even with a type-system handling the heavy lifting of static analysis, we still benefit from linting for legible, consistent style.

Fortunately for us, tslint is a first-rate TypeScript linter with configuration and use familiar from JavaScript tools like jshint or eslint. After installing tslint, and initializing a default configuration, we’re ready to lint a new or existing project:

$ npm install -g tslint
$ tslint --init
$ tslint -c tslint.json 'src/**/*.{ts,tsx}'

Setting up Jest

Jest is a low-configuration testing harness popular in React applications, and for good reason: in vanilla JavaScript, it mostly Just Works™. That’s a big deal for developers used to wrestling with PhantomJS (you know who you are–and if you don’t, just use Jest).

Using Jest with TypeScript takes a little more effort. Facebook’s Jest/TypeScript example outlines the strategy: set up a preprocessor for any TypeScript files; run Jest as before. ts-jest is a great option for preprocessing, providing compilation, source-mapping, and an all-around pleasant preprocessing experience. It’s available via npm:

$ npm install --save-dev ts-jest

Now, to start testing we need only add a new top-level entry for "jest" into the project’s package.json:

"jest": {
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js"
  ],
  "transform": {
    "\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
  },
  "testRegex": "/__tests__/.*\\.(ts|tsx|js)$"
}

While package.json is open, let’s add scripts for linting and testing to see all of our hard work in action:

"scripts": {
  "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'",
  "pretest": "npm run lint",
  "test": "jest"
},

Add some tests

At this point, our tests read the same as they would in JavaScript. Let’s write a simple spec that will verify future changes of our <Counter /> component using a snapshot made possible, as all good things are, by Jest.

// src/components/__tests__/counter_spec.tsx
import * as React from 'react'
import * as TestUtils from 'react-addons-test-utils'

import { createStore } from 'redux'

import { Counter } from '../counter'
import { reducers } from '../../reducers'

describe('<Counter />', () => {
  it('renders', () => {
    const store = createStore(reducers)
    expect(TestUtils.createRenderer().render(
      <Counter label='a counter!' store={store} />
    )).toMatchSnapshot()
  })
});

By rendering the component with a shallow renderer, we’re able to compare to a snapshot exactly as we would in JavaScript. All that’s left is to watch it turn green.

$ npm test

Conclusion

And there you have it: the basic tools, configurations, and scripts to make testing a central part of a TypeScript application. We don’t have to stop here! Though it’s an exercise for another day, the same techniques we use to validate code and write unit tests can just as easily apply in end-to-end testing.

The completed counter project is available for reference on github, tests and all. Have an comment, suggestion, or improvement? The issue queue is open for business. And I’m looking forward to your suggestions, experiences, and feedback over on twitter!

Happy testing!

Note: this is the fourth installment of a short series about bolstering Redux applications with TypeScript. Next, we’ll revisit our API interactions, using the redux-thunk middleware to tie them up. Read on!



TypeScript and Async Redux Actions

typescript and react

Note: this is the third entry in a short series about bolstering Redux applications with TypeScript. Part one introduced a simple counter application, which we then dressed up with a simple React UI. Read on!

We’ve previously put TypeScript together with Redux, then added a toy React application on top of it. What we haven’t considered yet is the world outside. Even toys, however, may need to store their state for safekeeping and recall at a later date. If you’re just looking to see this in action, check out the example project over on github. For the play-by-play, though, read on!

Actions

Our Redux application won’t be making direct calls to the API. Instead, we’ll dispatch actions to indicate that certain conditions have happened:

  • an asynchronous API call has been requested (X_REQUEST)
  • an asynchronous API call has succeeded (X_SUCCESS)
  • an asynchronous API call has failed (X_ERROR)

So far, we’re straight straight out of the excellent redux documentation. The only tricky question is, “how should we represent these actions in TypeScript?” And with TypeScript 2.0, we’re edging closer to a satisfying answer: “as a discriminate union.”

Here’s the idea. When the same string literal is present across multiple types, TypeScript can use it to narrow down the shape of the type:

type Foo = { type: 'FOO', str: string }
type Bar = { type: 'BAR': num: number }

type Action = Foo | Bar

function handle (a: Action): string {
  switch (a.type) {
    case 'FOO':
      return a.str + ' is a string!'
    case 'BAR':
      return a.num.toString() + 'is a number!'
  }
}

The catch–and it’s a big one–is that string literals mean quite a bit of boilerplate upfront. We need to explicitly declare the set of actions (one string literal), we won’t be able to set up dynamic (generic) action creators, and we need to rewrite the string any time it turns up in a loop or conditional expression.

There will probably be ways around this in the future. There may be ways around it now (and if you have one, I would love to hear from you). But for the time being we’re going to be doling out some redundant code.

In return for all of that cutting and pasting (incidentally, something that IDEs are really good at for–we’ll gain reasonable static guarantees throughout the async action flow.

Anyway, to the actions themselves. Now that we’re using discriminant unions, all actions will be attached to a single type. Call it Action. Since we’ve already touched on the three events generally involved with asynchronous actions, we can extend the union describing our actions so far with their implementations.

export type Action =

// UI actions
   { type: 'INCREMENT_COUNTER',
     delta: number }
|  { type: 'RESET_COUNTER' }

// Async actions...
| ({ type: 'SAVE_COUNT_REQUEST',
     request: { value: number } })
| ({ type: 'SAVE_COUNT_SUCCESS',
     request: { value: number },
     response: {})
| ({ type: 'SAVE_COUNT_ERROR',
     request: { value: number },
     error: Error })

There’s a general structure here that we’ll keep for all “groups” of actions describing asynchronous events.

  • every action includes a request field describing the original request
  • success actions include a response field to hold the asynchronous result
  • error actions include an error field to hold any errors that arise

It may make sense to structure these another way, depending on preference and application, but they should be consistent. As we’ll see in a moment, homogeneity here will simplify matters in other parts of the application.

Before we get there, note that we’ve already built up some of the boilerplate I promised. To keep things tidy as the list of actions grows, we can alias commonly-used types and use intersections to compose them.

type Q<T> = { request: T }
type S<T> = { response: T }
type E = { error: Error }

Here, Q<T> expresses actions containing requests, S<T> expresses those with responses, and E those that contain errors.

We can then add a few aliases for reused types of requests and responses.

type QEmpty = Q<null>
type QValue = Q<{ value: number }>

Here are the SAVE_COUNT_X actions rewritten using the tighter-if-marginally-more-opaque aliases. And since we’ll need them in a moment anyway, here are some additional actions (LOAD_COUNT_X) for comparison’s sake.

export type Action =
// ...
| ({ type: 'SAVE_COUNT_REQUEST' } & QValue)
| ({ type: 'SAVE_COUNT_SUCCESS' } & QValue & S<{}>)
| ({ type: 'SAVE_COUNT_ERROR'   } & QValue & E)

| ({ type: 'LOAD_COUNT_REQUEST' } & QEmpty)
| ({ type: 'LOAD_COUNT_SUCCESS' } & QEmpty & S<{ value: number }>)
| ({ type: 'LOAD_COUNT_ERROR'   } & QEmpty & E)

Async Action Creators

There’s an obvious relationship between the SAVE_COUNT_X actions, but we haven’t yet made it explicit to the type system. Let’s fix that.

export type ApiActionGroup<_Q, _S> = {
  request: (q?: _Q)         => Action & Q<_Q>
  success: (s: _S, q?: _Q)  => Action & Q<_Q> & S<_S>
  error: (e: Error, q?: _Q) => Action & Q<_Q> & E
}

export const saveCount: ApiActionGroup<{ value: number }, {}> = {
  request: (request) =>
    ({ type: 'SAVE_COUNT_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'SAVE_COUNT_SUCCESS', request, response }),
  error: (error, request) =>
    ({ type: 'SAVE_COUNT_ERROR',   request, error }),
}

export const loadCount: ApiActionGroup<null, { value: number }> = {
  request: (request) =>
    ({ type: 'LOAD_COUNT_REQUEST', request: null }),
  success: (response, request) =>
    ({ type: 'LOAD_COUNT_SUCCESS', request: null, response }),
  error: (error, request) =>
    ({ type: 'LOAD_COUNT_ERROR',   request: null, error }),
}

Now, when we need to refer to reference async actions, we can find them conveniently grouped within the saveCount and loadCount action groups.

We’re unfortunately packing on even more boilerplate. The action creators must return defined Actions using explicit string-literal types. We could conceivably work around this using dynamic action creators and a few generic-type hijinks, but as the code here is relatively simple and cut-and-paste operations are relatively cheap it may not be worth the trouble.

API

Our API will normally exist outside the redux application, or at least be the subject of a parallel design conversation. The persistence API for the counter has no surprises, however, so we’ve put off building it until now. We can use localStorage to achieve per-session persistence but still expose the same sort of promise-based API we might use to fetch data from a REST API or GraphQL server.

// ./api.ts
export const api = {

  // Save counter state
  save: (counter: { value: number }): Promise<null> => {
    localStorage.setItem('__counterValue', counter.value.toString())
    return Promise.resolve(null)
  },

  // Load counter state
  load: (): Promise<{ value: number }> => {
    const value = parseInt(localStorage.getItem('__counterValue'), 10)
    return Promise.resolve({ value })
  },
}

We’re leaning pretty heavily on the counter’s “toy” status to excuse the lack of validation, error handling, and formal request/response types. But, taking the focus back in the redux application, we now have something to call.

Middleware

Remember that the core application only dispatches actions, never interacting directly with the API? That’s a job for middleware. This is where we’ll finally see the awesomeness of those discriminant unions at work–and even make some API requests while we’re at it.

We’ll start with the code:

// ./middleware/index.ts
import * as redux from 'redux'

import { api } from '../api'

import {
  Action,
  saveCount,
} from '../actions'

export const apiMiddleware = ({ dispatch }: redux.MiddlewareAPI<any>) =>
  (next: redux.Dispatch<any>) =>
    (action: Action) => {
      switch (action.type) {
        case 'SAVE_COUNT_REQUEST':
          api.save(action.request)
            .then(() => dispatch(saveCount.success({}, action.request)))
            .catch((e) => dispatch(saveCount.error(e, action.request)))
          break

        case 'LOAD_COUNT_REQUEST':
          api.load()
            .then(({ value }) => dispatch(loadCount.success({ value }, action.request)))
            .catch((e) => dispatch(loadCount.error(e, action.request)))
          break
      }

      return next(action)
    }

This switch can be dressed up quite a bit; it may even go away entirely. We could build up a map of action groups to api methods, for instance, and iterate over them each time the middleware is called. But it helps to illustrate some of the magic that all of our legwork has been building towards.

Before, if we wanted to extract an action’s payload, we would need to switch on its type and then assert its type:

// No longer needed!

type SaveCountRequestAction = {
  type: 'SAVE_COUNT_REQUEST'
  request: { value: number }
}

// ...
case 'SAVE_COUNT_REQUEST':
  api.save({ value: (action as SaveCountRequestAction).request.value })
    // ...

This required us to re-establish the association between the action’s type and various other fields: redundant, intuitively unnecessary, and prone to fat fingers.

But using the union type, we can now reference action.request without a type assertion! Because both SAVE_COUNT_REQUEST and LOAD_COUNT_REQUEST actions do have .request fields, and TypeScript is able to narrow the set of matching types in each case based on its type, it also recognizes the shape of the corresponding action. If we wanted to, we could even extract the { value } attached to SAVE_COUNT_REQUEST without complaint from the compiler:

case 'SAVE_COUNT_REQUEST':
  const { value } = action.request
  api.save({ value })
    // ...

Conclusion

From there on out, things go back to normal. We map the actions’ dispatches to components, add reducers to update state, and map changes back to the components. The hum-drum is over in the example project.

But we’ve seen some pretty good stuff! We’ve set up asynchronous actions, and–in exchange for some boilerplate–gained reasonable static assurance that both the middleware and the core application have a handle on their shape and structure. The counter’s still just a toy, but the same strategies outlined here can be (in fact are being) used in much more sophisticated applications. Not a bad day’s work.

The completed counter project is available for reference on github, build, code, tests, and all. And I’m looking forward to your suggestions, experiences, and feedback over on twitter.

Note: this is the third installment of a short series about bolstering Redux applications with TypeScript. Next, we’ll shore things up with Jest-powered unit tests. Read on!



View all posts