Famini-ProDev
Posted on April 22, 2022
By default, Redux’s actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects. Redux also allows for middleware that sits between an action being dispatched and the action reaching the reducers.
There are two very popular middleware libraries that allow for side effects and asynchronous actions: Redux Thunk and Redux Saga. In this post, you will explore Redux Thunk.
Thunk is a programming concept where a function is used to delay the evaluation/calculation of an operation.
Redux Thunk is a middleware that lets you call action creators that return a function instead of an action object. That function receives the store’s dispatch method, which is then used to dispatch regular synchronous actions inside the function’s body once the asynchronous operations have been completed.
In this article, you will learn how to add Redux Thunk and how it can fit in a hypothetical Post List application.
.The first step:
Adding redux-thunk
First, use the terminal to navigate to the project directory and install the redux-thunk package in your project:
npm install redux-thunk@2.1.0
Now apply the middleware when creating your app’s store using Redux’s applyMiddleware. Given a React application with redux and react-redux, your configurationStore.js
file might look like this:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
const enhancer = applyMiddleware(thunk)
return createStore(
rootReducer,
initialState,
enhancer
);
}
Using Redux Thunk in a Sample Application
The most common use case for Redux Thunk is for communicating asynchronously with an external API to retrieve or save data. Redux Thunk makes it easy to dispatch actions that follow the lifecycle of a request to an external API.
showing a list of posts involves first dispatching an action. Then, if the list of post is successfully showed and returned by the external server, then list of posts are shown Otherwise an error message is displayed
Let’s see how this would be accomplished using Redux Thunk.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
componentDidMount() {
this.props.fetchData('https://jsonplaceholder.typicode.com/posts');
}
render() {
if (this.props.hasError) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<div style={setMargin}>
{this.props.items.map((item) => (
<div key={item.id}>
<ListGroup style={setDistanceBetweenItems}>
<ListGroupItem style={borderNone} header={item.title}>
<span className="pull-xs-right">Body: {item.body}</span>
</ListGroupItem>
</ListGroup>
</div>
))}
</div>
);
}
}
var setMargin = {
padding: "0px 200px 20px 200px"
};
var borderNone = {
border: "none",
background: "#fff"
};
var setDistanceBetweenItems = {
marginBottom: "5px",
padding: "30px",
paddingBottom: "50px",
background: "#fff"
};
ItemList.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasError: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasError: state.itemsHaveError,
isLoading: state.itemsAreLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
Fetching data from an API
Now onto our application. All of the above code snippets were just examples. We will now dive into the important bits of the code of our app. Also, a github repo will be available at the end of the article, containing the entire app.
Our app will fetch (asynchronously) data that is retrieved by an API — assuming we already built and deployed a working API, how convenient :) — and then display the fetched data as nice as my UI design skills go (not too far).
TVmaze’s public API contains tonnes of data and we will fetch all the shows they have ever aired. Then, the app will display all the shows, toghether with their rating and premiere date.
Designing our state
In order for this application to work properly, our state needs to have 3 properties: isLoading
, hasError
and items. So we will have one action creator for each property and an extra action creator where we will fetch the data and call the other 3 action creators based on the status of our request to the API.
Action creators
Let’s have a look at the first 3 action creators:
export function itemsHaveError(bool) {
return {
type: 'ITEMS_HAVE_ERROR',
hasError: bool
};
}
export function itemsAreLoading(bool) {
return {
type: 'ITEMS_ARE_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}
The last one will be called after the fetching was successful and will receive the fetched items as an parameter. This action creator will return an object with a property called items that will receive as value the array of items which were passed as an argument. Instead if items
: items
, we can just write items, using an ES6 syntactic sugar called property shorthand.
To visualize a bit what was described earlier, this is how it looks in Redux DevTools:
Out of the box, action creators can return just actions. That’s where Redux Thunk comes in handy. Thunk allows us to have action creators that return a function instead of an action and dispatch an action only in certain cases.
If it wasn’t for Redux Thunk, we would probably end up having just one action creator, something like this:
export function itemsFetchData(url) {
const items = axios.get(url);
return {
type: 'ITEMS_FETCH_DATA',
items
};
}
Obviously, it would be a lot harder in this scenario to know if the items are still loading or checking if we have an error.
Knowing these and using Redux Thunk, our action creator will be:
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsAreLoading(true));
axios.get(url)
.then((response) => {
if (response.status !== 200) {
throw Error(response.statusText);
}
dispatch(itemsAreLoading(false));
return response;
})
.then((response) => dispatch(itemsFetchDataSuccess(response.data)))
.catch(() => dispatch(itemsHaveError(true)));
};
}
Reducers
Now that we have our action creators in place, let’s start writing our reducers.
All reducers will be called when an action is dispatched. Because of this, we are returning the original state in each of our reducers. When the action type matches, the reducer does what it has to do and returns a new slice of state. If not, the reducer returns the original state back.
Each reducer takes 2 parameters: the (soon to be previous) slice of state and an action object:
export function itemsHaveError(state = false, action) {
switch (action.type) {
case 'ITEMS_HAVE_ERROR':
return action.hasError;
default:
return state;
}
}
export function itemsAreLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_ARE_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
Now that we have the reducers created, let’s combine them in our index.js
from our reducers
folder:
import { combineReducers } from 'redux';
import { items, itemsHaveError, itemsAreLoading } from './items';
export default combineReducers({
items,
itemsHaveError,
itemsAreLoading
});
Creating the store
Don’t forget about including the Redux Thunk middleware in the configureStore.js
:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// options like actionSanitizer, stateSanitizer
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
return createStore(
rootReducer,
initialState,
enhancer
);
}
Using the store in our root index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import ItemList from './components/ItemList';
const store = configureStore();
render(
<Provider store={store}>
<ItemList />
</Provider>,
document.getElementById('app')
);
Writing our React component which shows the fetched data
Let’s start by talking about what we are importing here.
In order to work with Redux, we have to import connect
from 'react-redux':
import { connect } from 'react-redux';
Also, because we will fetch the data in this component, we will import our action creator that fetches data:
import { itemsFetchData } from '../actions/items';
We are importing only this action creator, because this one also dispatches the other actions to the store.
Next step would be to map the state to the components’ props. For this, we will write a function that receives state
and returns the props object.
const mapStateToProps = (state) => {
return {
items: state.items,
hasError: state.itemsHaveError,
isLoading: state.itemsAreLoading
};
}
When we have a new state
, the props
in our component will change according to our new state.
Also, we need to dispatch our imported action creator.
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
With this one, we have access to our itemFetchData
action creator through our props
object. This way, we can call our action creator by doing
this.props.fetchData(url);
Now, in order to make these methods actually do something, when we export our component, we have to pass these methods as arguments to connect
. This connects our component to Redux.
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
Finally, we will call this action creator in the componentDidMount
lifecycle method:
this.props.fetchData('https://jsonplaceholder.typicode.com/posts');
Besides this, we need some validations:
if (this.props.hasError) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading ...</p>;
}
And the actual iteration over our fetched data array:
{this.props.items.map((item) => (
// display data here
))}
In the end, our component will look like this:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
componentDidMount() {
this.props.fetchData('https://jsonplaceholder.typicode.com/posts');
}
render() {
if (this.props.hasError) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading ...</p>;
}
return (
<div style={setMargin}>
{this.props.items.map((item) => (
<div key={item.id}>
<ListGroup style={setDistanceBetweenItems}>
<ListGroupItem header={item.title}>
<span className="pull-xs-right">Body: {item.body}</span>
</ListGroupItem>
</ListGroup>
</div>
))}
</div>
);
}
}
ItemList.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasError: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasError: state.itemsHaveError,
isLoading: state.itemsAreLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
And that was all !
You can see the full source of this project in : https://github.com/Famini-ProDev/PostList-redux-thunk
Posted on April 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.