What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests? OK, this is how Redux-thunk as one of the most used middlewares for Redux introduce itself: redux-thunk does this in a functional way, by passing them into your thunks (making them not exactly thunks at all, by dome definitions). I haven't worked with the other dispatch middleware approaches, but I assume they're basically the same. With Redux Thunk action creators can dispatch the result of other action creators and not even think whether those are synchronous or asynchronous:
Middleware like Redux Thunk or Redux Promise just gives you “syntax sugar” for dispatching thunks or promises, but you don’t have to use it.
So, without any middleware, your action creator might look like
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({
type: 'LOAD_DATA_SUCCESS',
data
}),
err => dispatch({
type: 'LOAD_DATA_FAILURE',
err
})
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
But with Thunk Middleware you can write it like this:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({
type: 'LOAD_DATA_SUCCESS',
data
}),
err => dispatch({
type: 'LOAD_DATA_FAILURE',
err
})
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
Also think about how this code will change. Say we want to have a second data loading function, and to combine them in a single action creator.
With the first approach we need to be mindful of what kind of action creator we are calling:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({
type: 'LOAD_SOME_DATA_SUCCESS',
data
}),
err => dispatch({
type: 'LOAD_SOME_DATA_FAILURE',
err
})
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({
type: 'LOAD_OTHER_DATA_SUCCESS',
data
}),
err => dispatch({
type: 'LOAD_OTHER_DATA_FAILURE',
err
})
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
With this approach, if you later want your action creators to look into current Redux state, you can just use the second getState
argument passed to the thunks without modifying the calling code at all:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({
type: 'LOAD_SOME_DATA_SUCCESS',
data
}),
err => dispatch({
type: 'LOAD_SOME_DATA_FAILURE',
err
})
);
}
}
If you need to change it to be synchronous, you can also do this without changing any calling code:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
Thunk functions can be used for both asynchronous and synchronous logic. Thunks provide a way to write any reusable logic that needs access to dispatch and getState. As it turns out, Redux already has an official version of that "async function middleware", called the Redux "Thunk" middleware. The thunk middleware allows us to write functions that get dispatch and getState as arguments. The thunk functions can have any async logic we want inside, and that logic can dispatch actions and read the store state as needed. Middleware also have access to dispatch and getState. That means you could write some async logic in a middleware, and still have the ability to interact with the Redux store by dispatching actions.
import {
client
} from '../api/client'
const delayedActionMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => { // Delay this action by one second next(action) }, 1000) return } return next(action)}const fetchTodosMiddleware = storeAPI => next => action => { if (action.type === 'todos/fetchTodos') { // Make an API call to fetch todos from the server client.get('todos').then(todos => { // Dispatch an action with the todos we received storeAPI.dispatch({ type: 'todos/todosLoaded', payload: todos }) }) } return next(action)}
const asyncFunctionMiddleware = storeAPI => next => action => { // If the "action" is actually a function instead... if (typeof action === 'function') { // then call the function and pass `dispatch` and `getState` as arguments return action(storeAPI.dispatch, storeAPI.getState) } // Otherwise, it's a normal action - send it onwards return next(action)}
const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware) const store = createStore(rootReducer, middlewareEnhancer) // Write a function that has `dispatch` and `getState` as argumentsconst fetchSomeData = (dispatch, getState) => { // Make an async HTTP request client.get('todos').then(todos => { // Dispatch an action with the todos we received dispatch({ type: 'todos/todosLoaded', payload: todos }) // Check the updated store state after dispatching const allTodos = getState().todos console.log('Number of todos after loading: ', allTodos.length) })}// Pass the _function_ we wrote to `dispatch`store.dispatch(fetchSomeData)// logs: 'Number of todos after loading: ###'
import {
createStore,
applyMiddleware
} from 'redux'
import thunkMiddleware from 'redux-thunk'
import {
composeWithDevTools
} from 'redux-devtools-extension'
import rootReducer from './reducer'
const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware)) // The store now has the ability to accept thunk functions in `dispatch`const store = createStore(rootReducer, composedEnhancer)export default store
import {
client
} from '../../api/client'
const initialState = []
export default function todosReducer(state = initialState, action) { // omit reducer logic}// Thunk functionexport async function fetchTodos(dispatch, getState) { const response = await client.get('/fakeApi/todos') dispatch({ type: 'todos/todosLoaded', payload: response.todos })}
Redux Middleware allows you to intercept every action sent to the reducer so you can make changes to the action or cancel the action.Middleware helps you with logging, error reporting, making asynchronous requests, and a whole lot more. All the middlewares mentioned in the applyMiddleware function will be executed one after the another. In the above code examples, we've created a single middleware. But you can create multiple middlewares and pass them to the applyMiddleware function like this: Once the middleware function is created, we pass it to the applyMiddleware function like this:
Take a look at the below code:
import React from "react";
import ReactDOM from "react-dom";
import {
createStore
} from "redux";
const reducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + action.payload;
case "DECREMENT":
return state - action.payload;
default:
return state;
}
};
const store = createStore(reducer);
store.subscribe(() => {
console.log("current state", store.getState());
});
store.dispatch({
type: "INCREMENT",
payload: 1
});
store.dispatch({
type: "INCREMENT",
payload: 5
});
store.dispatch({
type: "DECREMENT",
payload: 2
});
To create a middleware, we first need to import the applyMiddleware
function from Redux like this:
import {
applyMiddleware
} from "redux";
Let's say we're creating a loggerMiddleware
. Then to define the middleware we need to use the following syntax:
const loggerMiddleware = (store) => (next) => (action) => {
// your code
};
Once the middleware function is created, we pass it to the applyMiddleware
function like this:
const middleware = applyMiddleware(loggerMiddleware);
And finally, we pass the middleware to the createStore
function like this:
const store = createStore(reducer, middleware);
However, the flow of Redux’s state management tasks is completely synchronous: dispatching an action immediately generates the chain of calls to middleware and reducers to carry out the state transition. How do we integrate our application state with the data generated by an asynchronous action, while complying with Redux’s architectural pattern? However, this implementation violates the constraint that a reducer must be a pure function. In fact, by its nature, the result of an asynchronous task is based on a side effect.So, let’s take a look at a couple of valid solutions to this problem.
In order to solve our problem with asynchronous tasks, we can define an action as a function that starts an asynchronous task and delegates its execution to the Thunk middleware. Unlike the reducer, middleware is not required to be a pure function, so the Thunk middleware can perform functions that trigger side effects without any problems.
Let’s put these concepts into practice by implementing a simple application that shows a random Ron Swanson quote from a specialized API. The markup of the webpage appears as follows:
<div>
Ron Swanson says:
<blockquote id="quote"></blockquote>
</div>
For the JavaScript side, you need to get the redux
and redux-thunk
dependencies and import a few items in the module, as shown below:
import {
createStore,
applyMiddleware
} from 'redux';
import thunk from 'redux-thunk';
As stated before, you must first define three synchronous actions that represent changes in the state during the execution of the asynchronous task. Let’s define the following constants:
const QUOTE_REQUESTED = "QUOTE_REQUESTED";
const QUOTE_RECEIVED = "QUOTE_RECEIVED";
const QUOTE_FAILED = "QUOTE_FAILED";
Once we define the transformation of an asynchronous action into three synchronous actions, we need to manage their impact on state transitions. Let’s define the initial state of our application and the reducer that will manage quote retrieving:
const initialState = {
data: [],
status: ""
};
function quotes(state = initialState, action) {
switch (action.type) {
case QUOTE_REQUESTED:
state = Object.assign({}, state, {
status: "waiting"
});
break;
case QUOTE_RECEIVED:
state = Object.assign({}, state, {
data: […action.payload],
status: "received"
});
break;
case QUOTE_FAILED:
state = Object.assign({}, state, {
status: "failed",
error: action.payload
});
break;
}
return state;
}
The next step is to create the Redux store by specifying the use of the Thunk middleware, as shown by the following statement:
let store = createStore(quotes, initialState, applyMiddleware(thunk));
In this case, Redux middleware function provides a medium to interact with dispatched action before they reach the reducer. Customized middleware functions can be created by writing high order functions (a function that returns another function), which wraps around some logic. Multiple middlewares can be combined together to add new functionality, and each middleware requires no knowledge of what came before and after. You can imagine middlewares somewhere between action dispatched and reducer. Middlewares will let you write an action dispatcher which returns a function instead of an action object. Example for the same is shown below − Conditional dispatch can be written inside middleware. Each middleware receives store’s dispatch so that they can dispatch new action, and getState functions as arguments so that they can access the current state and return a function. Any return value from an inner function will be available as the value of dispatch function itself.
Commonly, middlewares are used to deal with asynchronous actions in your app. Redux provides with API called applyMiddleware which allows us to use custom middleware as well as Redux middlewares like redux-thunk and redux-promise. It applies middlewares to store. The syntax of using applyMiddleware API is −
applyMiddleware(...middleware)
And this can be applied to store as follows −
import {
createStore,
applyMiddleware
} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(rootReducer, applyMiddleware(thunk));
function getUser() {
return function() {
return axios.get('/get_user_details');
};
}
The getState function is useful to decide whether new data is to be fetched or cache result should get returned, depending upon the current state.
Let us see an example of a custom middleware logger function. It simply logs the action and new state.
import {
createStore,
applyMiddleware
} from 'redux'
import userLogin from './reducers'
function logger({
getState
}) {
return next => action => {
console.log(‘action’, action);
const returnVal = next(action);
console.log('state when action is dispatched', getState());
return returnVal;
}
}
Now apply the logger middleware to the store by writing the following line of code −
const store = createStore(userLogin, initialState = [], applyMiddleware(logger));
While Redux is synchronous you may wonder how does redux works with async operations. Redux Middleware is the answer. If you have the basic Redux store, it enables you to perform synchronous updates only. Middleware Redux is the right tool to extend the store’s capabilities. Redux middleware acts as a medium to interact with dispatched actions prior to the reducer. You can use Middlewares mainly to handle asynchronous actions in your application. Redux’s popular middleware libraries that allow for side effects and asynchronous actions are Redux-thunk and Redux-Saga. On the other hand, Middleware in React, though conceptually similar, has the capability to solve various different problems than Koa or Express. React Redux middleware extends a third-party extension point. This lies between dispatching the action and the reach to a reducer.
$ yarn add redux - thunk
Or using npm:
$ npm install redux - thunk
Let’s try to understand how ‘thunk’ actually works by implementing a small example of fetching the details of a particular user by hitting an API.
// Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import userDataReducer from './Reducer/reducer';
import {Provider} from 'react-redux';
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
const store= createStore(userDataReducer, applyMiddleware(thunk))
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
serviceWorker.unregister();
It returns a function that calls an API, which further returns a promise. As soon as it is resolved, it dispatches an action again. Further, it matches the corresponding action type. Eventually, it reaches the reducer.
import * as actionType from './actionType';
import reposApi from '../Api/repos';
export function getUserDetails() {
return (dispatch) => {
reposApi.getUserDetails()
.then(response => {
dispatch(getUserDetailsAction(response));
})
.catch(err => {
console.log("error occurred ====>>>", err);
})
}
}
export const getUserDetailsAction = item => {
return {
type: actionType.GET_PROFILE,
payload: item
}
}
Reducer updates the state object after performing the required calculation.
import * as actionType from "../Action/actionType";
const initialState = {
profile: {}
};
const userDataReducer = (state = initialState, action) => {
switch (action.type) {
case actionType.GET_PROFILE:
return {
...state,
profile: action.payload.data
};
default:
return state;
}
export default userDataReducer;