* this article assumes some basic knowledge in redux
Redux
Redux is a library commonly used for managing global state in React applications. Redux works great for state updates for synchronous actions (eg. incrementing/ decrementing a counter), but more often that not, most applications will need to perform some sort of asynchronous action (eg. making an API call to fetch data from the server).
redux-thunk
redux-thunk is a middleware that allows you to write asynchronous logic that interacts with the store. A redux middleware, as the name suggests, sits in the middle between the moment an action is dispatched, and the moment it reaches the reducer.
getting started
first, create your react app and install dependencies
npm install redux react-redux redux-thunk axios --save
or
yarn add redux react-redux redux-thunk axios
index.js
In your root index.js
file, import the Provider from 'react-redux' as per normal and wrap the App component with it so that the entire app has access to the redux store.
We will also need to import createStore from 'redux' as per normal. The only difference is that we also need to import applyMiddleware, a function from 'redux', and thunk from 'redux-thunk'. This will be passed in as the second argument when we create the store, so that whenever we dispatch an action, the action will be first sent to redux thunk as the middleware.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import App from './components/App';
import reducers from './reducers';
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root')
);
At this point, we will get some errors since we have not created our 'App' component and reducers. but first, let's do some configuration and create some action creators to fetch our data.
API configuration
While this step is not necessary, is is useful to create an axios instance and specify a base URL in an apis folder. By pre-configuring axios, we do not need to specify the base each time we make a request.
For this example, we will be fetching a list of posts from jsonplaceholder.
// src/apis/jsonPlaceholder.js
import axios from 'axios';
export default axios.create({
baseURL: 'https://jsonplaceholder.typicode.com'
})
action creators
the main difference between normal synchronous applications and asynchronous actions with redux thunk lies in this step.
generally, for redux, an action creator is simply a function that returns a plain javascript object with a type property (and occasionally some other properties such as 'payload' etc.)
with redux thunk, an action creator can also optionally return a function instead of an action object. our action creator to fetch posts would then look something like this:
// src/actions/index.js
import jsonPlaceholder from '../apis/jsonPlaceholder';
// normal action creator
export const clearPosts = () => ({
type: 'CLEAR_POSTS'
})
// thunk action creators
export const fetchPosts = () => async dispatch => {
const response = await jsonPlaceholder.get('/posts')
dispatch({type: 'FETCH_POSTS', payload: response.data})
}
export const fetchUser = id => async dispatch => {
const response = await jsonPlaceholder.get(`/users/${id}`)
dispatch({type: 'FETCH_USER', payload: response.data})
}
in addition to the dispatch argument, we can optionally pass in a second argument, getState, which would give us total control over changing or getting information out of our redux store.
// src/actions/index.js
export const fetchPostsAndUsers = id => async (dispatch, getState) => {
await dispatch(fetchPosts())
const userIds = _.uniq(_.map(getState().posts, 'userId'))
userIds.forEach(id => dispatch(fetchUser(id)))
}
reducers
nothing too different here.
// src/reducers/index.js
import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import userReducer from './userReducer';
export default combineReducers({
posts: postsReducer,
users: userReducer
});
// src/reducers/postsReducer.js
export default (state = [], action) => {
switch (action.type) {
case 'FETCH_POSTS':
return action.payload;
default:
return state;
}
};
// src/reducers/userReducer.js
export default (state = [], action) => {
switch (action.type) {
case 'FETCH_USER':
return [...state, action.payload];
default:
return state;
}
};
finally, our App.js
as per normal redux, we need to import connect from 'react-redux' in order to access the state in our redux store.
// src/components/App.js
import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';
class App extends React.Component {
componentDidMount() {
this.props.fetchPosts();
}
renderList() {
return this.props.posts.map(post => {
return (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
});
}
render() {
return <div>{this.renderList()}</div>;
}
}
const mapStateToProps = state => {
return { posts: state.posts };
};
export default connect(
mapStateToProps,
{ fetchPosts }
)(App);
Top comments (1)
// Step 1: Set up your Redux store
// store.js
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware)
);
export default store;
// Step 2: Create Redux Actions
// actions.js
export const FETCH_DATA_REQUEST = 'FETCH_DATA_REQUEST';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE';
export const fetchDataRequest = () => ({
type: FETCH_DATA_REQUEST,
});
export const fetchDataSuccess = (data) => ({
type: FETCH_DATA_SUCCESS,
payload: data,
});
export const fetchDataFailure = (error) => ({
type: FETCH_DATA_FAILURE,
payload: error,
});
// Step 3: Create a Redux Reducer
// reducers.js
import {
FETCH_DATA_REQUEST,
FETCH_DATA_SUCCESS,
FETCH_DATA_FAILURE,
} from './actions';
const initialState = {
data: [],
loading: false,
error: null,
};
const dataReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_DATA_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_DATA_SUCCESS:
return { ...state, loading: false, data: action.payload };
case FETCH_DATA_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
export default dataReducer;
// Step 4: Dispatch Actions
// actions.js
import {
fetchDataRequest,
fetchDataSuccess,
fetchDataFailure,
} from './actions';
// Example async action using Redux Thunk
export const fetchData = () => {
return async (dispatch) => {
dispatch(fetchDataRequest());
};
};
// Step 6: Connect Components
// DataComponent.js
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchData } from './actions';
const DataComponent = ({ data, loading, error, fetchData }) => {
useEffect(() => {
fetchData();
}, [fetchData]);
if (loading) {
Loading...;return
}
if (error) {
Error: {error};return
}
return (
Data
{data.map((item) => (- {item.name}
))}
);
};
const mapStateToProps = (state) => ({
data: state.data,
loading: state.loading,
error: state.error,
});
const mapDispatchToProps = {
fetchData,
};
export default connect(mapStateToProps, mapDispatchToProps)(DataComponent);