So you want to develop a nice React + Redux app but also take advantage of all the benefits TypeScript provides. We were in a similar situation recently here at OVO. Having used TypeScript in an existing Angular project, we were keen on using TypeScript together with React + Redux in a new project we were building. In this article we share a few suggestions following our own experience working with these technologies.
Create React App
If you are thinking of using Create React App to generate a nice boilerplate for your React app, consider using react-scripts-ts. It allows you to develop your app straight away without having to eject and setup typescript yourself. It also has TSLint already setup and it is regularly updated, so you shouldn't have to wait too long to get new features from the main Create React App project. If you want more control and prefer to setup TypeScript yourself, then you will have to eject from Create React App.
Components
Props
The way to interact with React components is using props, so we should make sure the props that a React component gets, are the props it expects. Before TypeScript or Flow, we did this using React's PropTypes, where we defined the type of props a particular component would expect. With TypeScript we don't need to use PropTypes anymore, we can just create an Interface which will define the type of props that a particular component should get.
interface ICounterProps {
count: number;
onDecrease: () => void;
onIncrease: () => void;
}
Stateless Functional Components
The majority of the components on a React project are typically stateless functional components, which main purpose is to render some UI based on the props it gets. We can use React.SFC and specify the interface for the props and make sure that it returns a JSX element. To have access to React's type definitions you must install @types/react. This is not needed for Redux or Reselect, since both packages include their own type definitions.
const Counter: React.SFC<ICounterProps> = ({
count,
onDecrease,
onIncrease
}): JSX.Element => {
return (
<div>
<button onClick={onDecrease}>-</button>
<span>{` ${count} `}</span>
<button onClick={onIncrease}>+</button>
</div>
);
};
React Components
On components where we want access to React's lifecycle events or need to have internal state, we use React.Component. To make sure that the internal state of the component is what we expect, we can also provide the interface for the state as the second argument.
interface ICounterState {
isDirty: boolean;
}
class Counter extends React.Component<ICounterProps, ICounterState> {
constructor(props: ICounterProps) {
super(props);
this.state = { isDirty: false };
}
public render(): JSX.Element {
const { count, onDecrease, onIncrease } = this.props;
return (
<div className="Counter" onClick={this.handleClick}>
<button onClick={onDecrease}>-</button>
<span>{` ${count} `}</span>
<button onClick={onIncrease}>+</button>
</div>
);
}
private handleClick = () => {
this.setState({ isDirty: true });
};
}
Redux
Redux is a very popular state management library based on the Flux architecture. By introducing TypeScript into a Redux app, it can really help to make it easier to reason with all of Redux's moving parts.
Actions
Every Redux action is identified by it's type. In a typical ES6 codebase we would probably create a const for each action type. In TypeScript we can use a string Enum instead, which allows us to group the constants together in a nicer way.
enum CounterActionTypes {
INCREASE_COUNT = "INCREASE_COUNT",
DECREASE_COUNT = "DECREASE_COUNT"
}
Now that we have created our action types, we should also create an interface for each action to make sure that every action that is dispatched has the type and payload we expect.
interface IIncreaseCountAction extends Action {
type: CounterActionTypes.INCREASE_COUNT;
}
We can now finally use our newly created action interfaces in the action creators, by providing them as an ActionCreator argument.
const increaseCount: ActionCreator<IIncreaseCountAction> = () => ({
type: CounterActionTypes.INCREASE_COUNT
});
Reducers
A Redux reducer is just a simple function that gets the current state and an action and returns the new state. Because a reducer can get different actions, we should create a new union type that specifies all the actions that the reducer can get. We should also make sure that the state is what we expect by creating an interface.
type CounterAction = IIncreaseCountAction | IDecreaseCountAction;
interface ICounterState {
count: number;
}
const initialState: ICounterState = {
count: 0
};
const reducer: Reducer<ICounterState> = (
state: ICounterState = initialState,
action: CounterAction | OtherAction
) => {
switch (action.type) {
case CounterActionTypes.INCREASE_COUNT:
return { count: state.count + 1 };
case CounterActionTypes.DECREASE_COUNT:
return { count: state.count - 1 };
default:
return state;
}
};
Selectors
Redux selectors are useful to compute derived data that we get from the data store. By using a library like Reselect we can also take advantage of selector composition and memoization. We can specify the interface for the input and output state on the Selector arguments.
const selectCounter = (state: IAppState): ICounterState => state.counter;
const selectCount: Selector<IAppState, number> = createSelector(
selectCounter,
counter => counter.count
);
TL;DR
These are just a few examples of how one can use TypeScript to improve a React + Redux project. By taking advantage of TypeScript features we get a more readable and manageable codebase and a great overall developer experience.