Taking React and Redux to the next level with Typescript
Leo Melo
Posted on September 2, 2019
Prologue
If you ever used Redux before you know that much of the ways we write Redux logic and why it works relies on us knowing the shape of our state ahead of time. That need is very much inline with how good typescript code forces us to define the shape of our functions and variables before we can build the output JavaScript code.
As I'll be making heavy use of Redux in the near future and I haven't done much with it for some time, I decided to go through Level Up Tutorials' (LUT) React and Redux For Everyone course to refresh my memory on the many concepts around Redux. To add some spice to it, and because I love TS, this time I decided I'd write the tutorial app in Typescript.
This post is a collection of thoughts and highlights of my experience.
Some example code
You can see the code for the course, and each step of my way via git tags, on my github. I've also created a CodeSandbox which contains a minimal setup for react-redux
and a connected component using Typescript.
You're free to look through them or use them as inspiration for your own code. I'll mostly use the repo on Github here to illustrate some points.
Defining the global state and root reducer
In my repo I had two reducers being merged by combineReducers
, their state is defined as follows:
-
movies
export interface IReduxMoviesState {
movies: IMovie[];
moviesLoaded: boolean;
moviesLoadedAt?: number;
movie?: IMovie;
movieLoaded: boolean;
}
-
toggle
export interface IReduxMessageState {
messageVisibility: boolean;
}
With our reducers returning each of these states, we can define the global app state like:
const rootReducer = combineReducers({
toggle,
movies
});
export type AppState = ReturnType<typeof rootReducer>;
This makes the AppState
look like:
type AppState = {
toggle: IReduxMessageState;
movies: IReduxMoviesState;
};
This is great because everywhere our redux state is used, we know exactly what it looks like and what we can reference from it when connecting components.
Defining action creators and action type constants
It's common practice in Redux to have action types being defined as constants. Because we're using Typescript, we can make use of enums and extending interfaces to make our code more descriptive. In my repo I have the following enum for action types:
export enum EReduxActionTypes {
GET_MOVIE = 'GET_MOVIE',
GET_MOVIES = 'GET_MOVIES',
RESET_MOVIE = 'RESET_MOVIE',
TOGGLE_MESSAGE = 'TOGGLE_MESSAGE'
}
If you're familiar with Typescript you'll see that I made the enums have defined values. This is to avoid the enum keys being assigned numerical values which could possibly make the code less resilient. Either way, this will make defining our action creators a little easier.
I defined the actions basing myself on an interface with a more generic type
value, it is pretty bare bones but it allows for great scalability:
export interface IReduxBaseAction {
type: EReduxActionTypes;
}
For example, in the case of the movies reducer, there are a few different actions that can be dispatched:
export interface IReduxGetMoviesAction extends IReduxBaseAction {
type: EReduxActionTypes.GET_MOVIES;
data: IMovie[];
}
export interface IReduxGetMovieAction extends IReduxBaseAction {
type: EReduxActionTypes.GET_MOVIE;
data: IMovie;
}
export interface IReduxResetMovieAction extends IReduxBaseAction {
type: EReduxActionTypes.RESET_MOVIE;
}
As with many things in Typescript, you don't need to know how the values for data are defined, all you need to know in this case is that each action will contain the correct type of object or array for the data
property of our action.
By aggregating those types into a union type, I can write my movies reducer like the below:
type TMoviesReducerActions = IReduxGetMoviesAction | IReduxGetMovieAction | IReduxResetMovieAction;
export default function(state: IReduxMoviesState = initialState, action: TMoviesReducerActions) {
switch (action.type) {
case EReduxActionTypes.GET_MOVIES:
return { ...state, movies: action.data, moviesLoaded: true, moviesLoadedAt: Date.now() };
case EReduxActionTypes.GET_MOVIE:
return { ...state, movie: action.data, movieLoaded: true };
case EReduxActionTypes.RESET_MOVIE:
return { ...state, movie: undefined, movieLoaded: false };
default:
return state;
}
}
This reducer is one of my favourite parts of this TS and Redux implementation.
Because I use different values of EReduxActionTypes
for each action. when I get action.data
within the different case
's, Typescript already knows that data is of the correct type, i.e. Imovie
for IReduxGetMovieAction
and IMovie[]
(an array of movies) for IReduxGetMoviesAction
.
This is a VERY POWERFUL THING.
In my tutorial app, the reducers are fairly simple but we can already see that scaling this wouldn't be much of an issue and wouldn't really increase the complexity of our store that much.
This is specially true if we take into account the excellent developer experience that VS Code offers to us for Typescript.
Connecting components and using our store's state
To connect our movies
state with a MoviesList
component, the code used is as follows:
const mapStateToProps = (state: AppState) => ({
movies: state.movies.movies,
isLoaded: state.movies.moviesLoaded,
moviesLoadedAt: state.movies.moviesLoadedAt
});
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
bindActionCreators(
{
getMovies
},
dispatch
);
export default connect(
mapStateToProps,
mapDispatchToProps
)(MoviesList);
There's quite a bit of code and re-assignment of values in here. Usually this could lead to some confusion as to which props are going to be available to our MoviesList
component but Typescript will make sure that doesn't happen by letting us parse the type definitions of mapStateToProps
and mapDispatchToProps
and use it when creating our component:
class MoviesList extends PureComponent<ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>, {}> {
// Code for the component goes here
}
We could even simplify things slightly by creating a MoviesList
props type like so:
type TMoviesListProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
class MoviesList extends PureComponent<TMoviesListProps, {}> {
// Code for the component goes here
}
Now, if we try to reference anything from this.props
inside our component, we will have full visibility of all the properties supplied to us by mapStateToProps
and mapDispatchToProps
.
Conclusion
Even though managing state with Redux and following its standard practices can lead us to spread logic through a number of files and/or add, an arguably large, amount of boilerplate code. By making use of Typescript, we can greatly increase the readability of our code and likely make it easier for anyone that may not be as aware of the ins and outs of a complex application, what each of its parts is responsible for and what they expect to receive from other components.
The tutorial application may not be the most complex one and maybe I didn't make the most elaborate use of Typescript. I still would like to think that it highlights some of Typescript's power and why more and more people are starting to look into it recently.
What do you think about Typescript and how it can change our developer experience when creating and scaling applications? Feel free to comment below or reach out to me on social media, details can be found on my website: leomeloxp.dev.
One last thing. When writing this app I tried to keep the code as close to the original code written in LUT's React and Redux for Everyone course as possible. If you like to learn more about the course or Level Up Tutorials in general feel free to visit their website.
This post was not sponsored by Level Up Tutorials, I just really like their content.
Posted on September 2, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.