React Redux A comparison between JS and TS
Starting Off
In this post I will do a side by side demonstration of setting up Redux with both JavaScript and TypeScript. TypeScript brings many benefits to your project but can be confusing your first time using it with React and Redux.
For this example I will be using the Internet Chuck Norris Database API from http://www.icndb.com/api/ to demonstrate API calls.
Setting up JavaScript
Run the following commands:
create-react-app redux-javascript
npm i -S redux redux-thunk react-redux
Setting up TypeScript
Run the following commands:
create-react-app redux-typescript --typescript
npm i -S redux redux-thunk react-redux
npm i -S @types/react-redux
Declare action types JavaScript
Declare some constants for our two actions. One for successful API calls and one for failing API calls.
//myActionTypes.js
export const API_SUCCESS = "API_SUCCESS";
export const API_FAILURE = "API_FAILURE";
Declare action types TypeScript
First we need to make some basic types that we will use to define the jokes and the state.
//myTypes.ts
export interface Joke {
type: string,
value: JokeValue
}
export interface JokeValue {
id: number,
joke: string,
categories: string[]
}
export interface JokeState {
successfulRequests: number,
unSuccessfulRequests: number,
jokes: Joke[]
}
In TypeScript we also need to define interfaces for our redux actions so we can declare the return types of our actions.
//myActionTypes.ts
export const API_SUCCESS = "API_SUCCESS";
export const API_FAILURE = "API_FAILURE";
interface GetJokeSuccessAction {
type: typeof API_SUCCESS,
joke: Joke,
number: number
};
interface GetJokeFailureAction {
type: typeof API_FAILURE,
number: number
};
We can export both interfaces as one type using | . This will make creating our reducer easier.
//myActionTypes.ts
export type JokeActionTypes = GetJokeSuccessAction | GetJokeFailureAction;
Setup Reducer JavaScript
We need to import our Constants (ActionTypes) and declare our initial state (the initial state could be in it’s own file, but I’m leaving it here for this example).
//reducers.js
import { API_SUCCESS, API_FAILURE } from "./myActionTypes";
const initialState = {
successfulRequests: 0,
unSuccessfulRequests: 0,
jokes: []
};
Now we setup our reducer. We need to send what we want the new state to be in each case. If it’s successful add the joke to the jokes array and increment the successful counter. If it’s not successful we need only to increment the unsuccessful counter. And as always remember to return the unaffected state if none of the actions in this reducer are needed.
//reducers.js
export function jokesReducer(state = initialState, action) {
//successful action called
if (action.type === API_SUCCESS) {
let totalSuccess = state.successfulRequests + action.number;
return {
...state,
successfulRequests: totalSuccess,
jokes: [...state.jokes, action.joke]
};
}
//failure action called
else if (action.type === API_FAILURE) {
let totalFailures = state.unSuccessfulRequests + action.number;
return { ...state, unSuccessfulRequests: totalFailures };
}
return state;
}
Setup Reducer TypeScript
Add initial state to the reducer like before only this time we need to set the type.
//reducers.ts
import { JokeState } from "./myTypes";
import { API_SUCCESS, API_FAILURE, JokeActionTypes } from "./myActionTypes";
const initialState: JokeState = {
successfulRequests: 0,
unSuccessfulRequests: 0,
jokes: []
};
This is where we benefit from combining the two actions as one type. We need to define the type of actions and we can use our JokeActionTypes type.
//reducers.ts
export function jokesReducer(state = initialState, action: JokeActionTypes) {
if (action.type === API_SUCCESS) {
let totalSuccess = state.successfulRequests + action.number;
return {
...state,
successfulRequests: totalSuccess,
jokes: [ ...state.jokes , action.joke]
};
} else if (action.type === API_FAILURE) {
let totalFailures = state.unSuccessfulRequests + action.number;
return { ...state, unSuccessfulRequests: totalFailures };
}
return state;
}
Setup Store JavaScript
We start off by creating our rootReducer.
//store.js
import { createStore, compose, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import { jokesReducer } from "./reducers";
const rootReducer = combineReducers({
jokes: jokesReducer
});
Then we need to enable the Redux Dev tools (you’ll also need the Redux Devtools Chrome Extension installed).
//store.js
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
Now we make our configure store function.
//store.js
export default function configureStore() {
return createStore(
rootReducer,
compose(applyMiddleware(thunk), composeEnhancers())
);
}
Setup Store TypeScript
Here we create the rootReducer like before only this time, we need to make a new type (in this example I named it state) to represent the store in the application.
//store.ts
import { createStore, compose, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import { jokesReducer } from "./reducers";
const rootReducer = combineReducers({
jokes: jokesReducer
});
export type State = ReturnType<typeof rootReducer>;
This time we also need to add the Redux devtools to the window.
//store.ts
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
}
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
Configuring the store is the exact same as in the JavaScript version.
Setup API JavaScript
Now we just need to make a function to call the API in our apiService.js file. In this function we will simply be using a fetch statement to call the API.
//apiService.js
export function getJoke() {
return fetch("http://api.icndb.com/jokes/random?limitTo=[nerdy]")
.then(response => response.json())
.catch(error => Promise.reject(error));
}
Setup API TypeScript
Now we need to setup the API call. In this case we need to declare that it returns a Promise of type Joke.
//apiService.ts
import { Joke } from "../store/myTypes";
export function getJoke(): Promise<Joke> {
return new Promise((resolve,reject)=>{
fetch("http://api.icndb.com/jokes/random?limitTo=[nerdy]")
.then(response=> response.json())
.catch(error => reject(error))
.then(body => resolve(body))
});
};
Setup Actions JavaScript
To begin we need to import our ActionType Constants and create the actions that will be modifying the state.
//actions.js
import { API_SUCCESS, API_FAILURE } from "./myActionTypes";
import * as apiService from "../services/apiService";
export function successfulApiCall(joke) {
return {
type: API_SUCCESS,
joke: joke,
number: 1
};
}
export function failedApiCall() {
return {
type: API_FAILURE,
number: 1
};
}
Then we will create our thunk action that dispatches the other two actions based on the API response.
//actions.js
export function fetchJoke() {
return function(dispatch) {
return apiService
.getJoke()
.then(result => dispatch(successfulApiCall(result)))
.catch(e=>dispatch(failedApiCall()));
};
}
Setup Actions TypeScript
Setting up actions is as about as simple as it is in JavaScript--we just need to add types.
//actions.ts
import { JokeActionTypes, API_SUCCESS, API_FAILURE } from "./myActionTypes";
import { Joke } from "./myTypes";
import { ThunkAction } from "redux-thunk";
import { State } from "./store";
import { Action } from "redux";
import * as apiService from "../services/apiService";
export function successfulApiCall(joke: Joke): JokeActionTypes {
return {
type: API_SUCCESS,
joke: joke,
number: 1
};
}
export function failedApiCall(): JokeActionTypes {
return {
type: API_FAILURE,
number: 1
};
}
Something to point out is the ThunkAction type on our redux-thunk function that calls the API.
//actions.ts
export function fetchJoke(): ThunkAction<void, State, null, Action<string>> {
return function(dispatch) {
return apiService
.getJoke()
.then(result => dispatch(successfulApiCall(result)))
.catch(e=>dispatch(failedApiCall()));
};
}
Connect Store to App.js
This is the exact same in both minus the React.FC on the component. All we need to do is import the configureStore function we created and the Provider component from react-redux. Then we just use them.
//App.js
const store = configureStore();
function App() {
return (
<Provider store={store}>
<Home />
</Provider>
);
}
export default App;
In the Component JavaScript
We’ll be making a Home component to display our work.
We will need to import the following items into our component. We need to import connect and bindActionCreators so that we can interact with our redux store as props.
//Home.js
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { fetchJoke } from "../store/actions";
This is how we connect it.
//Home.js
const mapStateToProps = state => {
return {
jokes: state.jokes.jokes
};
};
export default connect(mapStateToProps, dispatch =>
bindActionCreators({ fetchJoke: fetchJoke }, dispatch)
)(Home);
Here is the component. It is pretty basic but suits our needs for this demonstration.
//Home.js
const Home = props => {
return (
<div className="App">
<button onClick={() => props.fetchJoke()}>GetJoke</button>
{props.jokes.map((joke, key) => (
<p key={key}>{joke.value.joke}</p>
))}
</div>
);
};
In the Component TypeScript
In this case we also have to import some of our types. We also need to declare prop types.
//Home.tsx
//Additional Imports
import { State } from "../store/store";
import { Joke } from "../store/myTypes";
interface HomeProps {
jokes: Joke[];
fetchJoke: typeof fetchJoke;
}
There are only minor differences with the component and mapStateToProps to point out.
//There are only slight differences to declaring the component
//and mapStateToProps
const Home: React.FC<HomeProps> = props => {
return (
<div className="App">
<button onClick={() => props.fetchJoke()}>GetJoke</button>
{props.jokes.map((joke, key) => (
<p key={key}>{joke.value.joke}</p>
))}
</div>
);
};
const mapStateToProps = (state: State) => {
return {
jokes: state.jokes.jokes
};
};
Finishing Up
TypeScript is more verbose, but overall it’s not too much more complicated. The benefits that it provides definitely outweigh the cost and I would definitely recommend the TypeScript approach.
And remember “Chuck Norris can compile syntax errors”.