Why do we need middleware for async flow in Redux?
Why do we need middleware for async flow in Redux? ๐
Have you ever wondered why Redux requires middleware to support asynchronous data flow? ๐ค The answer lies in the fundamental nature of Redux and how it handles actions and reducers.
According to the Redux documentation, "Without middleware, Redux store only supports synchronous data flow." But why is this the case? ๐คทโโ๏ธ Why can't we simply call the async API directly from the container component and dispatch the actions? ๐ฆ๐ฅ
Let's look at a simple example to illustrate the problem. Imagine a user interface with a field and a button. When the user clicks the button, the field should be populated with data fetched from a remote server. ๐ฒ๐
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
// Action Types
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
// Async API
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
// App Component
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
// App Component PropTypes
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
// Reducer
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
// Store
const store = Redux.createStore(reducer);
// Connected App Component
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
}
)(App);
// Exported Component
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
In this example, we have the App
component which contains the field, button, and a loading indicator. When the button is clicked, it triggers the update
function provided by the connect
function from react-redux
. This function dispatches an action to inform the App
component that it is updating, and then makes an asynchronous API call to fetch the field value. Once the call is completed, another action is dispatched with the fetched value as the payload. ๐ฎ
So, what's wrong with this approach? Why do we need middleware such as Redux Thunk or Redux Promise, as the documentation suggests? ๐ค
The answer lies in the requirement for action creators to be pure functions. In the past, only pure functions were allowed as action creators. However, this limitation was relaxed, as discussed in several GitHub issues (source). While action creators can now be impure functions, pure functions are still preferred for better predictability and testability. ๐งช
By using middleware, such as Redux Thunk or Redux Promise, we can separate the impure logic from the pure logic within an action creator. The impure logic, such as making API calls or handling promises, can be encapsulated in the middleware, while the action creator itself remains a pure function. ๐งฉโจ
For example, with Redux Thunk, we can rewrite the update
function as follows:
const update = () => {
return (dispatch) => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
};
};
The update
function now returns a thunk, which is a function that takes the dispatch
function as an argument. This allows us to dispatch multiple actions or handle asynchronous operations within a single action creator. The thunk middleware handles the execution of the thunk, invoking the function with the dispatch
argument. ๐ฎ๐
So, even though Redux no longer strictly requires action creators to be pure functions, using middleware for handling async flow offers several benefits. It helps to separate concerns, improves code maintainability, and allows for better testing of pure action creators. ๐ ๏ธ๐งช
In conclusion, middleware is needed for async flow in Redux because it helps handle asynchronous operations within action creators, separates concerns, maintains code purity, and enhances testing capabilities. ๐๐ฆโจ
Do you have any questions or suggestions related to this topic? Feel free to leave a comment below and let's spark a conversation! ๐ฃ๏ธ๐ฌ