Why I Stopped Using Redux

Gabriel Abud - Jul 6 '20 - - Dev Community

Redux was a revolutionary technology in the React ecosystem. It enabled us to have a global store with immutable data and fixed the issue of prop-drilling in our component tree. For sharing immutable data across an application, it continues to be an excellent tool that scales really well.

But why do we need a global store in the first place? Are our frontend applications really that complex or are we trying to do too much with Redux?

The Problem with Single Page Applications

The advent of Single Page Applications (SPAs) such as React, brought about a lot of changes to how we develop web applications. Separating our backend from our frontend code allowed us to specialize and separate out concerns. It also introduced a lot of complexity, namely around state.

Fetching data asynchronously now meant that the data had to live in two places: the frontend and the backend. We have to think about how best to store that data globally so it's available to all of our components, while maintaining a cache of the data to reduce network latency. A big part of frontend development now becomes burdened with how to maintain our global store without suffering from state bugs, data denormalization, and stale data.

Redux is not a Cache

The main problem most of us get into when using Redux and similar state management libraries is that we treat it as a cache for our backend state. We fetch data, add it to our store with a reducer/action, and refetch it periodically to make sure it's up to date. We are making Redux do too much and using it as a catch-all solution to our problems.

One important thing to remember is that our frontend and backend state are never really in sync, at best we can create a mirage that they are. This is one of the downsides of the client-server model and why we need a cache in the first place. Caching and maintaining state in sync is immensely complex however, so we shouldn't be recreating this backend state from the ground up like Redux encourages us to.

The line between backend and frontend responsibility quickly becomes blurred when we start to recreate our database on the frontend. As frontend developers, we shouldn't need to have a thorough knowledge of tables and their relationships in order to create a simple UI. Nor should we have to know how best to normalize our data. That responsibility should fall on the people designing the tables themselves - the backend developers. Backend developers can then provide an abstraction for the frontend developers in the form of a documented API.

There are now a myriad of libraries (redux-observable, redux-saga, and redux-thunk to name a few) built around Redux to help us manage data from the backend, each adding a layer of complexity to an already boilerplate-heavy library. I believe most of these miss the mark. Sometimes we need to take a step back before taking a step forward.

What if we stop trying to manage our backend state in our frontend code and instead treat it like a cache that just needs to be updated periodically? By treating our frontends like simple display layers that read from a cache, our code becomes significantly easier to work with and more accessible to pure frontend developers. We get all of the benefits of separating out concerns without most of the downsides of building SPAs.

A Simpler Approach to Backend State

There are a couple of libraries that I believe are a huge improvement over using Redux (or similar state management library) for storing backend state.

React Query

I've been using React Query for a few months in most of my personal and work projects. It's a library with a very simple API and a couple of hooks to manage queries (fetching data) and mutations (changing data).
Since using React Query, not only am I more productive but I end up writing 10x less boilerplate code than I would have with Redux. I find it easier to focus on the UI/UX of my frontend applications without having to keep the whole backend state in my head.

To compare this library to Redux, it helps to see an example of the two methods in code. I've implemented a simple TODO list fetched from the server with both methods, using vanilla JS, React Hooks, and axios.

First, the Redux implementation:

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from 'axios';

const SET_TODOS = "SET_TODOS";

export const rootReducer = (state = { todos: [] }, action) => {
  switch (action.type) {
    case SET_TODOS:
      return { ...state, todos: action.payload };
    default:
      return state;
  }
};

export const App = () => {
  const todos = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  useEffect(() => {
    const fetchPosts = async () => {
      const { data } = await axios.get("/api/todos");
      dispatch({
        type: SET_TODOS,
        payload: data}
      );
    };

    fetchPosts();
  }, []);

  return (
    <ul>{todos.length > 0 && todos.map((todo) => <li>{todo.text}</li>)}</ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

Note that this doesn't even begin to handle refetching, caching, and invalidation. This simply loads the data and stores it in your global store on load.

Here's the same example implemented with React Query:

import React from "react";
import { useQuery } from "react-query";
import axios from "axios";

const fetchTodos = () => {
  const { data } = axios.get("/api/todos");
  return data;
};

const App = () => {
  const { data } = useQuery("todos", fetchTodos);

  return data ? (
    <ul>{data.length > 0 && data.map((todo) => <li>{todo.text}</li>)}</ul>
  ) : null;
};
Enter fullscreen mode Exit fullscreen mode

By default this examples includes data refetching, caching, and stale invalidation with pretty sensible defaults. You can set the caching configuration at the global level and then forget about it - in general it will do what you expect. For more on how this works under the hood, check out the React Query docs. There are a ton of configuration options available to you, this only begins to scratch the surface.

Anywhere you need this data, you can now use the useQuery hook with the unique key you set (in this case "todos") and the async call to use to fetch the data. As long as the function is asynchronous, the implementation doesn't matter - you could just as easily use the Fetch API instead of Axios.

For changing our backend state, React Query provides the useMutation hook.

I've also written a curated list of React Query resources that you can find here.

SWR

SWR is conceptually almost identical to React Query. React Query and SWR were developed around the same time and both influenced each other in positive ways. There is also a thorough comparison between these two libraries in the react-query docs.

Like React Query, SWR also has really readable documentation. For the most part, you can't go wrong with either library. Regardless of what ends up becoming the norm in the near future, it will be much easier to refactor from that than the equivalent Redux mess.

Apollo Client

SWR and React Query focus on REST APIs, but if you need something like this for GraphQL the leading contender is Apollo Client. You'll be pleased to learn that the syntax is almost identical to React Query.

What About Frontend State?

Once you start using one of these libraries, you will find that on the vast majority of projects, Redux is overkill. When the data fetching/caching part of your app is taken care of, there is very little global state for you to handle on the frontend. What little amount is left can be handled using Context or useContext + useReducer to make your own pseudo-Redux.

Or better yet, use React's built in state for your simple frontend state. There is nothing inherently wrong with that.

// clean, beautiful, and simple
const [state, setState] = useState();
Enter fullscreen mode Exit fullscreen mode

Let's embrace the separation of backend from frontend more fully instead of remaining in this ambiguous in-between state. These up and coming libraries represent a shift in how we manage state in single page applications and are a big step in the right direction. I'm excited to see where they lead the React community.

. . . . . . .
Terabox Video Player