Tutorial: TypeScript, React, and Redux
- 8/17/2016
- ·
- #typescript
- #redux
- #react
- #howto
This is the second entry in a short series about bolstering Redux application with TypeScript. Part one introduced a simple counter application; next, we dress it up with a simple UI powered by React. If you’re in a hurry, skip on over to the finished project on Github; everyone else, 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-specificConnectedState
.mapDispatchToProps
will let us dispatch new actions to alter application state through an implementation ofConnectedDispatch
.connect
will wrapCounterComponent
and return a component implementingOwnProps
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.
Up Next: this is the second part of a short series about building Redux applications with TypeScript. Next, we’ll extend the application with a async API actions. Read on!