How To Use Redux with Hooks in a React-TypeScript Project
Gerald
Posted on September 18, 2020
Introduction
Redux is a predictable state container for JavaScript applications. In this tutorial, I will show you how to use redux to manage state in React with TyepeScript and Hooks.
Getting Started
If you are only interested in viewing complete code on GitHub, click here. Otherwise, let's setup the project using Create React App. In this tutorial I will be using yarn but you should be fine with npm as well. In your terminal run the following command
npx create-react-app posts --typescript
This command creates a React Typescript project called posts. To start the development server and view the project in your browser, run the following commands.
cd posts
yarn start
Installations
To use redux:
yarn add @reduxjs/toolkit
To use Redux with React and TypeScript:
yarn add react-redux
yarn add @types/react-redux
To add redux thunk:
yarn add redux-thunk
To add redux devtools:
yarn add redux-devtools-extension
Redux
Setup your redux folder as follows
src
-redux
--actions
--effects
--interfaces
--reducers
--store
--types
The interfaces folder is used for adding all interfaces that can be used across the project. For this tutorial, we will use posts fake data from JSONPlaceholder. In interfaces directory, create a file called Post.ts and add the following code.
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
The interface above defines a single post.
Now we need to setup our types. In the types folder, create a file called PostTypes.ts and add the following code
import { Post } from '../interfaces/Post';
export const GET_POSTS = 'GET_POSTS';
export interface GetPostsStateType {
posts: Post[];
}
interface GetPostsActionType {
type: typeof GET_POSTS;
payload: Post[];
}
export type PostActionTypes = GetPostsActionType;
GetPostsStateType interface is defining what the state will look like; an array of posts. GetPostsActionType interface is defining the action type that you will see later in this tutorial.
In the reducers directory, create a file called PostReducer.ts and add the following code
import {
GET_POSTS,
GetPostsStateType,
PostActionTypes
} from '../types/PostTypes';
const initialStateGetPosts: GetPostsStateType = {
posts: []
};
export const getPostsReducer = (
state = initialStateGetPosts,
action: PostActionTypes
): GetPostsStateType => {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: action.payload
};
default:
return state;
}
};
In here, we initialize state of type GetPostsStateType that we defined earlier. We then create a reducer function called getPostsReducer. A reducer takes two parameters; state and action. In our case, state and action are of types initialStateGetPosts and PostActionTypes respectively while the reducer function returns GetPostsStateType. In the switch block, if the case is GET_POSTS, we return whatever is there in the state and update it with the new payload and the default case is state. Note that in a bigger project there would be a lot of cases.
Create another file in the reducers folder and lets call it index.ts. In here, we will combine all our reducers using combineReducers and export them as rootReducer[You can call it anything really] as shown below.
import { combineReducers } from 'redux';
import { getPostsReducer } from './PostReducer';
const rootReducer = combineReducers({
posts: getPostsReducer
});
export default rootReducer;
Now we will create our store. A store holds the whole state tree of the application. In the store folder, let's have index.ts and add the following code:
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import rootReducer from '../reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunkMiddleware))
);
export type AppState = ReturnType<typeof rootReducer>;
export default store;
All we are doing in here is creating a store from the combined reducers called rootReducer. composeWithDevTools will allow you to monitor global state in your browser if you've installed the Redux Devtools Extension. applyMiddleware(thunkMiddleware) allows us to dispatch async actions
To make the store available to React components, in src/index.ts, we wrap App in Provider and pass the store as shown below
import { Provider } from 'react-redux';
import store from './redux/store';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
At this point, you should see your global state with an empty array of posts as shown below.
The only way to change the state in the store is through an action dispatch. In the actions folder create PostActions.ts and add the following code:
import { GET_POSTS, PostActionTypes } from '../types/PostTypes';
import { Post } from '../interfaces/Post';
export const getPostsAction = (posts: Post[]): PostActionTypes => {
return {
type: GET_POSTS,
payload: posts
};
};
The getPostsAction function accepts an array of posts and returns a type of GET_POSTS and posts data passed to the payload variable. Note that type and payload can be given names of your choice.
To fetch our posts from the fake API, let's create Posts.ts in the effects folder and add the following code.
import { getPostsAction } from '../actions/PostActions';
import { Dispatch } from 'redux';
import { PostActionTypes } from '../types/PostTypes';
export const getPosts = () => {
return function (dispatch: Dispatch<PostActionTypes>) {
const POST_URL = 'https://jsonplaceholder.typicode.com/posts';
fetch(POST_URL, {
method: 'GET'
})
.then(res => res.json())
.then(data => {
dispatch(getPostsAction(data));
return data;
});
};
};
All we are doing here is dispatching the getPostsAction and passing it the data from the fake API.
React Component
Finally, in App.ts, we can access our App State. Update App.ts as follows:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getPosts } from './redux/effects/Posts';
import { Post } from './redux/interfaces/Post';
import { AppState } from './redux/store';
export default function Posts() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
const posts = useSelector((state: AppState) => state.posts);
const postItems = posts.posts.map((post: Post) => (
<div key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
));
return <div>{postItems}</div>;
}
In here, we bring in useDispatch and useSelector from react-redux. The useDispatch hook is used to dispatch actions as needed. In our case, we are passing the getPosts effect to dispatch in the useEffect hook. This will add the data coming from the fake API to our redux store as soon as the App component mounts. At this point your redux store should look like this:
useSelector works more or less like mapStateToProps when using connect. It allows us to access app state in a React functional component. In our case we are interested in getting posts from the posts state and that is exactly why we are iterating through posts.posts. Then we display the post title with post.title and body with post.body. Interesting right?
Conclusion
There are many ways you could use redux in your React project. Go with a setup that works for you. Redux can have a lot of boilerplate but comes in handy once the boilerplate code is out of the way.
Happy coding!
Posted on September 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.