TypeScript, Redux, and React

typescript and react

Note: this is the second entry in a short series about bolstering Redux application with TypeScripts. Part one introduced a simple counter application; next, we dress it up with a simple UI powered by React. Read on!

A web application without a UI isn’t much to look at. We previously used TypeScript to add static type-checking to a redux application, but with I/O limited to console.log its usability left something to be desired.

We can fix at least one shortcoming by adding a React-powered view layer. We’ll also get to see how TypeScript works out in a real (if trivial) interface, and use its static type-checking to verify our work at compile-time.

If you’re just interested in the results, check out the finished counter over on github. Otherwise, it’s time to get to work.

Setting up

We’ll need a few small new dependencies on top of our base redux project to get things working with react.

$ npm install --save-dev react react-dom react-redux

Since this is TypeScript, we’ll also need type definitions for any packages that don’t include them. At time of writing, that’s pretty much everything except redux, but there are community-supported alternatives available from npm and the DefinitelyTyped project for most of it. We’ll use typings to download them:

$ npm install -g typings
$ typings init
$ typings install --save --global dt~react
$ typings install --save --global dt~react-dom
$ typings install --save npm~react-redux

Note: DefinitelyTyped typings are evolving quickly. Please check out this article’s companion project for a more-recent configuration for the project’s type declarations.

We now have a typings.json and a typings directory containing the updated definitions. Next, manually edit tsconfig.json to enable JSX and include the root typings definition:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "ES5",
    "jsx": "react"
  },
  "files": [
    "typings/index.d.ts"
  ]
}

Finally, we need to tweak the webpack configuration to handle .tsx files. There’s a detailed guide over in the typescript handbook, but to get up and running we can update webpack.config.js to read:

module.exports = {
  entry: './src/index.tsx',
  output: {
    filename: './dist/bundle.js',
  },
  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js']
  },

  module: {
    loaders: [{ test: /\.tsx?$/, loader: 'ts-loader' }]
  },

  externals: {
    'react': 'react',
    'react-dom': 'reactdom'
  }
};

Components

A counter is simple enough. We’ll need a label (“what are we counting?”), a display (“what’s the count?”), and some way to increment it (“how does the count change?”). We can provide them with a simple component implementation in src/components/counter.tsx:

import * as React from 'react'

interface OwnProps {
  label: string
}

interface ConnectedState {
  counter: { value: number }
}

interface ConnectedDispatch {
  increment: (n: number) => void
}

interface OwnState {}

class CounterComponent extends React.Component<ConnectedState & ConnectedDispatch & OwnProps, OwnState> {

  _onClickIncrement = () => {
    this.props.increment(1)
  }

  render () {
    const { counter, label } = this.props
    return <div>
      <label>{label}</label>
      <pre>counter = {counter.value}</pre>
      <button ref='increment' onClick={this._onClickIncrement}>click me!</button>
    </div>
  }
}

The component’s internal state is easy: we won’t be maintaining any. OwnState is an empty interface included for demonstration, but we could just as easily satisfy TypeScript with an anonymous object:

class CounterComponent extends React.Component<..., {}> {
  // ...
}

The props are a bit more involved. this.props will be an object implementing the merged ConnectedState, ConnectedDispatch, and OwnProps interfaces. The separation may seem a bit excessive for such a trivial component, but–as we’ll see in a moment–there’s a reason for it.

Even though we haven’t outlined where the props come from yet, they’re clearly setting up for something. “Something” is a connection between CounterComponent and the application store, and as soon as we add in react-redux, some magical things will happen.

Connection

Here’s the plan. react-redux exposes a Provider component that–when supplied with a redux store–will allow container components wrapped to hook into the redux data flow. All we need to do is wrap the component in the connect method and implement two hooks to interact with the application:

import * as React from 'react'
import * as redux from 'redux'
import { connect } from 'react-redux'

import { incrementCounter } from '../actions'
import { Store } from '../reducers'

const mapStateToProps = (state: Store.All, ownProps: OwnProps): ConnectedState => ({
  counter: state.counter,
})

const mapDispatchToProps = (dispatch: redux.Dispatch<Store.All>): ConnectedDispatch => ({
  increment: (n: number) => {
    dispatch(incrementCounter(n))
  },
})

class CounterComponent extends React.Component<ConnectedState & ConnectedDispatch & OwnProps, OwnState> {
  // ...
}

export const Counter: React.ComponentClass<OwnProps> =
  connect(mapStateToProps, mapDispatchToProps)(CounterComponent)

TypeScript’s impact is typically minimal. Aside from the type annotations this is exactly what we would write in JavaScript. Given the global state:

  • mapStateToProps will extract relevant data into an object implementing the component-specific ConnectedState.

  • mapDispatchToProps will let us dispatch new actions to alter application state through an implementation of ConnectedDispatch.

  • connect will wrap CounterComponent and return a component implementing OwnProps

That’s exactly the same as in JavaScript. We export the wrapped component for use elsewhere in the application, where it will now have be able to access the redux store.

The Counter in Action

Let’s get to counting. All we need is a new store, a <Provider /> component, and a call to ReactDOM.render(). We’ll set it all up in src/index.tsx (our webpack entry point):

import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

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

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

let store: Redux.Store<Store.All> = Redux.createStore(reducers)

ReactDOM.render(
  <Provider store={store}>
    <Counter label='a counter' />
  </Provider>
, document.body)

And that’s it! Webpack the app, load dist/bundle.js in an HTML page, and things will be up and counting in no time.

In conclusion

Why all the ceremony?

We’ve added an awful lot of verbosity just to see it vanish from the compiled app. Type definitions and explicit signatures have ancillary value as supplemental documentation, but there are concrete benefits as well.

Let’s run a little experiment. We’ll remove the Counter component’s label in the finished app, just to see what happens.

<Provider store={store}>
  <Counter />
</Provider>

It’s easy enough to forget a prop when writing in JavaScript, but this sort of thing breeds runtime errors down the line. With static type-checking, however, TypeScript will refuse to compile the application with the label missing:

ERROR in ./src/index.tsx
(20,7): error TS2324: Property 'label' is missing in type
  'IntrinsicAttributes & IntrinsicClassAttributes<Component<OwnProps, {} | void>>
  & OwnProps & { chi...'.

Repeat the experiment with actions, stores, reducers–if we accidentally pass the wrong parameters through the application, TypeScript will let us know.

This isn’t to say that typing will do away with runtime errors entirely, though with proper setup it can help reduce their incidence. Even beyond the benefits of (somewhat limited) type-safety, the documentation we gain around the structure of application components will improve visibility and ease maintenance down the line. TypeScript is a tool, as are Flow and Dart. It’s no replacement for thoughtful development, but used appropriately they can all shine light into dark corners and provide a more pleasant development experience for everyone on the team.

And there you have it, the long story behind a counter demo. The completed project is available for reference on github, and I’m looking forward to your suggestions, experiences, and feedback over on twitter.

Note: this is the second part of a short series about bolstering Redux applications with TypeScript. Next, we’ll extend the application with a async API actions. Read on!

Let's stay in touch! Share this post or subscribe for (very) occasional updates.

Hey, I'm RJ! Writer, speaker, sustainable development advocate, and inconsistent micropoet, broadcasting live from Portland.

Posted 8/16/2016
Tags
Also