TypeScript and Redux
React and Redux with Typescript
My notes from the Udemy course by Stephen Grider. Please watch the course first, then come back for the notes
Please watch the course first, and come back for the notes after!
Pros and cons of using Typescript with React
The pros:
- helps you avoid typing errors,
- helps you understand the code organisation
- more safety when refactoring
The cons:
- Need to add a lot of generics
- Lots of importing of files to access the types
The basics
Annotating the props of a class component
Create an interface
, and use it as a generic for your component to describe the props.
If you hover over React.Component
, you will see that the generic can actually take two arguments, the first is the typing of the props, the second is for it's internal state. More on this below.
interface AppProps {
color: string;
}
class App extends React.Component<AppProps> {
render() {
return <div>{this.prpos.color}</div>
}
}
ReactDOM.render(<App color="red" />, document.getElementById('app'));
Annotating the state of a class component
There are actually two ways to initialise the state of a class. The "classic" way is via the constructor
and super
call. With this syntax, you are actually extending the component's state
property:
interface AppState {
counter: number;
}
class App extends React.Component<AppProps, AppState> {
constructor(props: AppProps) {
super(props);
this.state = { counter: 0 };
}
//...
render() {
//...
}
}
In this case, you need to provide the AppState
interface as a second argument to the generic.
When setting state with classProperties
AFAIK, it's only in a proposal state, but you can use this syntax via Babel. In this pattern, you don't have to do the constructor
call, just set the state directly:
class App extends React.Component<AppProps> {
this.state = { counter: 0 };
//...
render() {
//...
}
}
With this pattern you are completely overriding the state
property of the React component, including it's own internal definition of it and so the second argument to the generic is not necessary, so above we only have <AppProps>
.
Annotating functional components
You annotate the function arguments and provide a return type:
interface AppProps {
color: string;
}
function App(props: AppProps): JSX.Element {
return <div>{prpos.color}</div>
}
ReactDOM.render(<App color="red" />, document.getElementById('app'));
Redux and Typescript
npm install redux react-redux axios redux-thunk
And the basic Redux setup
import React from 'react';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { reducers } rom './reducers';
// 1. create the store, will define the reducers later
const store = createStore(reducers, applyMiddleware(thunk));
// 2. wrap your <App /> with Provide
// passing it the store (createStore)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app');
);
And create the reducers:
// src/reducers/index.ts
import { combineReducers } from 'redux';
export const reducers = combineReducers({
counter: () => 1
})
The action creators
We will be using thunks for asynchronous actions. A thunk is a function that returns another function and it's used by the redux-thunk middleware to do async actions. The function returned by the thunk takes a dispatch function as an argument that you can call when the async process is finished.
This is one of the harder parts of using Typescript with Redux.
import axions from 'axios';
// 2. import Dispatch to annotate
// the dispatch argument of the thunk
import { Dispatch } from 'redux';
export function fetchTodos() {
// 1. annotate the dispatch argument
// Stephen found it in the type definitions file
return async function(dispatch: Dispatch) {
const response = await axios.get(url);
dispatch({
type: 'FETCH_TODOS',
payload: response.data
});
}
}
Let's add the annotations:
import axions from 'axios';
import { Dispatch } from 'redux';
// 2. create the Todo interface
interface Todo {
id: number;
title: string;
completed: boolean;
}
export function fetchTodos() {
return async function(dispatch: Dispatch) {
// 1. annotate the promise return value
// via a generic
const response = await axios.get<Todo[]>(url);
dispatch({
type: 'FETCH_TODOS', // next we will deal with this, see below
payload: response.data // now Typescript now that this is a Todo[]
});
}
}
Action types enum
The common pattern is to use an enum
to list the different action types to centralise it for your application.
// **** src/actions/types.ts ****
export enum ActionTypes {
fetchTodos
}
Usually, you would expect some more verbose declaration like so:
enum ActionTypes {
fetchTodos = 'fetchtodos'
}
With the enum
type, we don't really need to provide an actual value for our actiontype. We never actually use the fetchtodos
string directly, only the reference to it (ActionTypes.fetchTodos
). Typescript will internally assign some id to it, and it's sufficent for our purposes.
Now, we can reference this action on our dispatch
:
//...
dispatch({
type: ActionTypes.fetchTodos,
payload: response.data
})
Annotating the dispatch call
The dispatch
(from Redux) can take a generic that would describe the action we want it to dispatch. This can some extra checking to your code:
import axions from 'axios';
import { Dispatch } from 'redux';
interface Todo {
// ...
}
// 1. define the action interface
interface FetchTodosAction {
type: ActionTypes.fetchTodos;
payload: Todo[];
}
export function fetchTodos() {
return async function(dispatch: Dispatch) {
const response = await axios.get<Todo[]>(url);
// 2. add the generic
dispatch<FetchTodosAction>({
type: ActionTypes.fetchTodos,
payload: response.data
});
}
}
This will guard you against some typos when you call the dispatch.
Aside on organising the action creators files
In a normal Javascript file, you could include several of these action creators functions (thunks, or just simple functions). But with Typescript, because of the type annotations (interface Todo
above), it's better to create a separate file per each action creator.
Annotating the reducer
Again, you need to import some files, so that you use the types for the annotation:
// **** src/reducers/todos.ts ****
import { Todo, FetchTodosAction } from '..actions';
import { ActionTypes } from '../actions/types';
export function todosReducer(
state: Todo[] = [], // default value for the app init
action: FetchTodosAction // can reuse the interface from above
) => {
switch(action.type) {
case ActionTypes.fetchTodos:
return action.payload;
default:
return state;
}
}
Note, that when Redux boots up, it also sends some internal actions that will go through your reducers. So the above action: FetchTodosAction
annotation is not totally correct, it doesn't cover these internal Redux actions.
Annotating the Redux store
The combineReducers
function accepts a generic that you can use to describe the possible shape of the store:
// **** src/reducers/index.ts ****
import { todosReducer } from './todos';
import { Todo } from '../actions';
export interface StoreState {
todos: Todo[]
}
export const reducers = combineReducers<StoreState>({
todos: todosReducer
});
Now, Typescript will check that todosReducer
will actually redurn an Todo[]
(array of Todo
items).
Also, the StoreState
definition will help other developers to understand the entire, possible states of our application.
Connecting and rendering the Redux store
As before, we will annotate the props that our App component can receive, create the mapStateToProps and mapDispatchToProps functions and connect our App.
import React from 'react';
import { connect } from 'react-redux';
import { Todo, fetchTodos } from '..actions';
import { StoreState } from '..reducers';
// 1. create interface for the generic
// that will be passed to the component
interface AppProps {
todos: Todo[];
fetchTodos(): any; // just an any for now, will correct
}
// 2. pass the AppProps as a generic
class _App extends React.Component<AppProps> {
// 5. now can fetch todos:
onButtonClick = (): void => {
this.props.fetchTodos();
}
render() {
//...
// render the list (pseudo code)
this.props.todos.map((todo: Todo) => {
return <div key={todo.id}>{todo.title}</div>
});
}
}
// 3. map.state.to.props:
// the StoreState is form our combineReducers declaration
function mapStateToProps(state: StoreState): { todos: Todo[] } {
return { todos: state.todos };
}
// 4. then connect our app
export const App = connect(
mapStateToProps,
{ fetchTodos } // reminder: see AppProps above
)(_App);
File organisation
Best to export all the action creators into separate files. So for example, create src/actions/todos.ts
and put all the todo related actions there. The index.ts
now should just contain export * from './todos.'
exports.
And finally, now you can just import { Todo, FetchTodosAction, ActionTypes } from '../actions'
, everything you need is there.
Type guards in reducers
The switch
statement inside the reducers act as an implicit type guards.
So in the different cases inside the switch
, you have a guarantee of the state
and payload
"shape".
// **** src/reducers/todos.ts ****
import { Todo, FetchTodosAction, ActionTypes } from '..actions';
export function todosReducer(
state: Todo[] = [],
action: FetchTodosAction
) => {
switch(action.type) {
case ActionTypes.fetchTodos:
return action.payload;
default:
return state;
}
}
AppProps interface
This has not been explained in the course, but this is how the final interface
for AppProps
looks like in App.tsx
:
// **** App.tsx *****
// ...
interface AppProps {
todos: Todo[];
fetchTodos: typeof fetchTodos;
deleteTodo: typeof deleteTodo;
}
// ...
export const App = connect(
mapStateToProps,
{ fetchTodos }
)(_App);
However, this creates a mismatch in Typescript, in the connect
call. deleteTodo
is a "normal" action creator that returns a simple object, but fetchTodos
is a Redux Thunk, returning a function, that takes a dispatch argument and returns a Promise.
This is a case in Typescript, where we cannot be really more specific and cannot tell exactly what fetchTodos
reall is. So we change the annotation to:
interface AppProps {
todos: Todo[];
fetchTodos: Function;
deleteTodo: typeof deleteTodo;
}
Initialising a React Component
Just repeating.
class _App extends React.Component<AppProps, AppState> {
// B. or you can just do
state: { fetching: false }
// but it would override the React internal state
// A. extending the React state
constructor(props: AppProps) {
super(props);
this.state = { fetching: false };
}
// just an example of a lifecycle method usage
componentDidUpdate(prevProps: AppProps): void {
if (!prevProps.todos.length && this.props.todos.length) {
this.setState({ fetching: false });
}
}
}