close
close
actions must be plain objects. use custom middleware for async actions.

actions must be plain objects. use custom middleware for async actions.

3 min read 09-12-2024
actions must be plain objects. use custom middleware for async actions.

Redux, a predictable state container for JavaScript apps, relies on a fundamental principle: actions must be plain JavaScript objects. This seemingly simple rule is crucial for maintainability, predictability, and the efficient functioning of the Redux ecosystem. However, handling asynchronous operations within this framework requires a bit more finesse. This article delves into the importance of plain object actions and demonstrates how custom middleware can elegantly manage asynchronous action creators.

Why Plain Objects?

The insistence on plain objects for actions isn't arbitrary. It provides several key benefits:

  • Simplicity and Predictability: Plain objects are easy to understand, debug, and serialize. This simplicity reduces complexity and makes your codebase more maintainable. Avoiding functions or classes as actions eliminates unexpected behavior caused by closures or hidden side effects.

  • Serialization: Redux often relies on serialization (converting data structures to a format for storage or transmission), particularly when using tools like Redux DevTools. Plain objects serialize consistently and reliably, whereas custom objects might require special handling.

  • Middleware Compatibility: Redux middleware (functions that intercept and modify actions) are designed to work with plain objects. Complex action structures could break or interfere with the functionality of middleware.

The Challenge of Asynchronous Actions

While plain objects are ideal, handling asynchronous operations (like API calls) directly within action creators violates the plain object rule. Directly using async/await or promises inside an action creator would create a non-plain object.

Incorrect Approach (Avoid This):

// Incorrect: This action creator is NOT a plain object
async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return { type: 'FETCH_USER_SUCCESS', user: data };
}

The Solution: Custom Middleware

The elegant solution is to use custom middleware. Middleware sits between action dispatch and the reducer, allowing you to intercept and modify actions before they reach the reducer. This enables you to handle asynchronous logic outside the action creator, keeping actions themselves plain objects.

Implementing Async Middleware

Here's an example of a custom middleware function that handles asynchronous actions:

const asyncMiddleware = store => next => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState); // If it's a function, execute it
  }
  return next(action); // Otherwise, pass it through normally
};

This middleware checks if the dispatched 'action' is a function. If it is, it executes the function, providing store.dispatch and store.getState as arguments. This allows the function to dispatch further actions (e.g., success or failure actions) after the asynchronous operation completes.

Using the Middleware and Thunk Action Creators

Now, let's create an asynchronous action creator using the "thunk" pattern (a common approach):

export const fetchUser = userId => async (dispatch, getState) => {
  dispatch({ type: 'FETCH_USER_REQUEST', userId }); // Dispatch a request action

  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    dispatch({ type: 'FETCH_USER_SUCCESS', user: data });
  } catch (error) {
    dispatch({ type: 'FETCH_USER_FAILURE', error });
  }
};

Notice that fetchUser is now a function that returns another function. This returned function receives dispatch and getState as arguments. Inside this function, we dispatch plain object actions to indicate request, success, and failure states.

Finally, apply this middleware to your Redux store:

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import asyncMiddleware from './asyncMiddleware';

const store = createStore(rootReducer, applyMiddleware(asyncMiddleware));

This setup ensures that all asynchronous action creators are properly handled without compromising the plain object requirement for actions.

Conclusion

By adhering to the rule that actions must be plain objects and leveraging custom middleware like the example above, you create a more robust, predictable, and maintainable Redux application. This approach cleanly separates asynchronous logic from action creators, ensuring your Redux store remains a source of truth that is both simple and efficient. Remember, the simplicity of plain objects is key to the power and elegance of Redux.

Related Posts


Popular Posts