An Introduction to Redux-Logic: Powerful Middleware for Handling Async Actions

Redux has become the go-to state management solution for many React applications. By providing a predictable state container and a simple flow for updating state, Redux makes it easier to write maintainable apps as they grow in size and complexity.

However, one limitation of Redux out of the box is that it only supports synchronous updates to state. In the real world, apps frequently need to handle asynchronous actions, such as fetching data from an API or interacting with browser APIs. This is where middleware like Redux-Logic comes in.

In this article, we‘ll take an in-depth look at Redux-Logic – a powerful and flexible middleware for handling async actions in Redux. We‘ll cover what it is, how it works, and why you might choose it over other middleware options. We‘ll also walk through a complete example of using Redux-Logic in a real application.

Whether you‘re new to Redux or have experience with other async middleware, this guide will give you a solid understanding of Redux-Logic and how to use it effectively in your projects. Let‘s dive in!

What is Redux Middleware?

Before we get into Redux-Logic specifically, let‘s quickly review what middleware is in the context of Redux.

In Redux, actions are synchronously dispatched to the store, which then invokes the root reducer to update state. Middleware sits between the action being dispatched and the reducer. It can inspect actions, do some processing, dispatch other actions, call APIs, and ultimately pass the action to the reducer (or not).

Redux data flow with middleware

Some common uses for middleware include:

  • Logging actions for debugging
  • Performing side effects like analytics tracking
  • Dispatching async actions and handling the response
  • Modifying or enhancing actions
  • Blocking or altering actions conditionally

Redux middleware is configured when setting up the store with createStore(). Multiple middleware can be combined in a chain, where each middleware function can choose to pass an action to the next.

The most common async middleware for Redux is redux-thunk, which allows you to dispatch functions (thunks) that can perform async logic and dispatch actions. Other popular options include redux-saga and redux-observable.

Introducing Redux-Logic

Redux-Logic is a Redux middleware that aims to make handling async actions as simple and declarative as possible. It was created by Jeff Barczewski, who wanted a more powerful alternative to redux-thunk that supported real-world use cases.

Here are some of the key features of Redux-Logic:

  • Intercept and act on any action
  • Declarative processing using RxJS observables
  • Cancellation of async processes
  • Debouncing of actions
  • Built-in async await support
  • Take latest option to only use the latest action
  • Decouples async code from components

At a high level, Redux-Logic allows you to create "logic" files adjacent to your reducers. These logic files export arrays of logic objects that describe how to handle actions of certain types.

When an action is dispatched, Redux-Logic will check if any logic matches that action. If so, it will execute that logic and use the result to dispatch a new action, which then flows through the rest of the Redux update cycle.

Setting Up Redux-Logic

To use Redux-Logic, you first need to add the redux-logic package to your project:

npm install --save redux-logic

Then in your store configuration, import createLogicMiddleware from redux-logic and applyMiddleware from Redux. Create the logic middleware and apply it when creating the store:

import { createLogicMiddleware } from ‘redux-logic‘;
import { createStore, applyMiddleware } from ‘redux‘;
import rootReducer from ‘./rootReducer‘;
import arrLogic from ‘./logic‘;

const logicMiddleware = createLogicMiddleware(arrLogic);

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

The createLogicMiddleware function takes an array of logic objects, which we‘ll look at next. You can also pass it additional options to configure things like action debouncing and limiting.

Creating Logic

Once the Redux-Logic middleware is set up, you can define logic objects that describe how to handle specific actions. Here‘s an example logic that fetches posts from an API:

import { createLogic } from ‘redux-logic‘;

const fetchPostsLogic = createLogic({
  type: ‘FETCH_POSTS‘, // Action this logic handles

  processOptions: {
    dispatchReturn: true,
    successType: ‘FETCH_POSTS_SUCCESS‘, 
    failType: ‘FETCH_POSTS_FAILURE‘
  },

  process({ httpClient, getState, action }, dispatch, done) {
    httpClient.get(`https://jsonplaceholder.typicode.com/posts`)
      .then(resp => resp.data)
      .then(posts => dispatch({
        type: ‘FETCH_POSTS_SUCCESS‘,
        payload: posts
      }))
      .catch((err) => {
        console.error(err);
        dispatch({
          type: ‘FETCH_POSTS_FAILURE‘,
          payload: err,
          error: true
        })
      })
      .then(() => done()); // call done when finished dispatching
  }
});

export default [fetchPostsLogic];

Let‘s break this down:

  • type specifies the action type this logic will intercept
  • processOptions configures how Redux-Logic handles the result of process
  • successType is the action to dispatch if the API call succeeds
  • failType is the action to dispatch if the API call fails
  • process is where the async logic happens. It receives dependencies like httpClient to make API requests and the original action
  • dispatch and done allow you to dispatch new actions and tell Redux-Logic when you‘re done processing

The process function uses a promise chain to make the API request and dispatch a success or failure action with the result. Dispatching is handled by Redux-Logic according to the processOptions.

Using the Logic

With the fetchPostsLogic defined, components can initiate the process of fetching posts by simply dispatching a plain FETCH_POSTS action:

import React from ‘react‘;
import { useSelector, useDispatch } from ‘react-redux‘;

function Posts() {
  const posts = useSelector(state => state.posts);
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch({ type: ‘FETCH_POSTS‘ });
  }, []);

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

When this component mounts, it dispatches FETCH_POSTS. The fetchPostsLogic intercepts that action, makes the API call, and dispatches either FETCH_POSTS_SUCCESS or FETCH_POSTS_FAILURE with the result. The reducer can listen for those actions to update the posts state.

This decoupling of async logic from components is one of the key benefits of Redux-Logic. Components don‘t need to know anything about how data is fetched, and the logic can be reused across the app.

Why Use Redux-Logic?

We‘ve seen how Redux-Logic makes it easy to declaratively handle async actions, but what advantages does it offer over other middleware options? Let‘s compare it to a few of the most popular alternatives.

Redux-Thunk

Redux-Thunk is a simple middleware that allows you to dispatch functions (thunks) in addition to plain action objects. The thunk function receives dispatch and getState, allowing it to perform async logic and dispatch actions as a result.

const fetchPosts = () => async dispatch => {
  const posts = await fetch(‘https://jsonplaceholder.typicode.com/posts‘)
    .then(response => response.json());

  dispatch({ type: ‘FETCH_POSTS_SUCCESS‘, payload: posts });
};

Thunks are a simple way to handle async actions, but they have a few drawbacks:

  • Async logic is embedded in action creators, making them harder to test and reuse
  • No declarative way to debounce, take latest, or cancel async actions
  • Difficult to handle failure actions and other cross-cutting concerns

Redux-Saga

Redux-Saga is a powerful middleware that uses ES6 generators to make it easy to handle complex async workflows.Sagas can listen for actions and then execute async logic using blocking functions like call, put, and take.

function* fetchPostsSaga() {
  try {
    const posts = yield call(fetch, ‘https://jsonplaceholder.typicode.com/posts‘);
    yield put({ type: ‘FETCH_POSTS_SUCCESS‘, payload: posts });
  } catch (error) {
    yield put({ type: ‘FETCH_POSTS_FAILURE‘, error });
  }
}

function* rootSaga() {
  yield takeLatest(‘FETCH_POSTS‘, fetchPostsSaga);
}

Sagas offer a lot of control flow options like taking latest, parallel execution, and cancellation. However, they require understanding ES6 generators which have a steeper learning curve. The indirection of dispatching actions to trigger sagas can also make the logic harder to follow.

Redux-Observable

Redux-Observable is a middleware that uses RxJS to make it easy to handle async actions as streams of actions. Epics listen for actions, perform async logic, and emit new actions using a set of powerful RxJS operators.

const fetchPostsEpic = action$ => action$.pipe(
  ofType(‘FETCH_POSTS‘),
  mergeMap(action => ajax.getJSON(‘https://jsonplaceholder.typicode.com/posts‘).pipe(
    map(response => ({ type: ‘FETCH_POSTS_SUCCESS‘, payload: response })),
    catchError(error => of({ type: ‘FETCH_POSTS_FAILURE‘, error }))
  ))
);

Redux-Observable offers declarative composition and powerful operators, but it requires a separate RxJS dependency and deep knowledge of observables, which can be a hurdle for teams.

The Benefits of Redux-Logic

In comparison to those other async middleware options, Redux-Logic offers several compelling benefits:

  • Plain actions – Logic is triggered by the same plain action objects that Redux already uses, making it easy to understand and debug.
  • Declarative – Logic objects describe how to handle actions in a clear, composable way without getting bogged down in control flow.
  • Powerful – Redux-Logic supports cancellation, debouncing, limiting, and other advanced async use cases without needing to learn a separate paradigm.
  • Flexible – Logic objects can choose what to do with actions, including dispatching, doing other processing, calling APIs, or doing nothing at all.
  • Testable – Async logic is decoupled from components and reducers, making it easier to test in isolation.

For many apps, Redux-Logic provides a "just right" balance between the simplicity of thunks and the power of sagas or observables. And by building on plain Redux actions, it offers a cohesive model for handling complex async logic.

Recap & Next Steps

We‘ve covered a lot of ground in this introduction to Redux-Logic! To recap, here are some of the key points:

  • Redux-Logic is a middleware for handling async actions and complex side effects in Redux
  • It allows you to define "logic" objects that describe how to intercept and handle actions declaratively
  • Logic can perform async operations like API calls and dispatch new actions with the results
  • Redux-Logic offers powerful features like cancellation, debouncing, and limiting out of the box
  • It integrates seamlessly with Redux by building on plain action objects

If you‘re building a Redux app that needs to handle async actions, I highly recommend giving Redux-Logic a try. Its declarative, action-based model provides a cohesive way to manage side effects as your app grows in complexity.

To learn more, check out the official Redux-Logic documentation and examples. You can also find the complete code for the posts example from this article here.

If you have any questions or thoughts on Redux-Logic, leave a comment below. Thanks for reading, and happy coding!

Similar Posts