Lightweight Alternatives to Redux

Lightweight Alternatives to Redux

When building a React application, there are many options to choose from when it comes to a state management library. Redux has been the go-to choice for many development teams over the years. However, React's ever evolving ecosystem and unopinionated nature provide various solutions where Redux might not be the best choice.

In this article, let's look at these lightweight alternatives to Redux that you can use in your React applications to manage the application state.

React Hooks

Depending on the React application, it might not be a state management library. Hooks available in React brought the state to functional components. It's a powerful feature that allows a function component to manage its own local state. Not all applications would require having everything in a central state. It might be over-engineered for such cases.

You can create a local state management system for the functional component using React hooks such as useState, useEffect, ' useReducer'. These hooks are available in React out of the box. You can create custom hooks and separate them from the presentational components to make the code more readable.

React Context and useReducer hook

There are two types of states to deal with in React apps. The first type is the local state that is used only within a React component. The second type is the global state that can be shared among multiple components within a React application.

Another API that React ships out of the box is Context. It falls under the second type of shared state category. Using it with the useReducer hook, you can implement a custom global lightweight state management system. The useReducer hook is a great way to manage complex state objects and state transitions. It is different from useState. It updates the state by accepting a reducer function and an initial state. Then, it returns the actual state and a dispatch function. This dispatch function is used to make changes to the state.

If you have used Redux in the past, that may sound familiar. Conceptually, Redux uses the same concept of dispatching a function to make changes.

With an example, let's see you can use them together. Start by importing the required hooks from React library.

import { useReducer, createContext } from 'react';

Next, create an empty context and an initial state object. If there is no initial state, you can leave this object empty. For example, in the code snippet below, we've defined an initial state with a todos array with a single item.

export const TodosContext = createContext();

const initialState = {
  todos: [
    {
      id: 0,
      item: 'Buy a notebook'
    }
  ]
};

The next step is to define a function called reducer. It is going to take two arguments, the current state, and action. This function is used to update the state object whenever there is an action dispatched. The action dispatches when the app user performs an activity with it. One example of an action is a user adding more items to the todos array.

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        todos: [...state.todos, action.payload]
      };
    default:
      return {
        state
      };
  }
};

Next, define a TodoContextProvider that will be the central store.

export const TodosContextProvider = props => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <TodosContext.Provider value={[state, dispatch]}>
      {props.children}
    </TodosContext.Provider>
  );
};

The useReducer hook creates a reducer using the reducer function defined in the previous code snippet. This reducer is the first argument to the hook. The initialState is passed as the second argument.

After that, you will have to wrap the set of components or the initial App component of the React application with the TodosContextProvider. This way, the Context API makes the state and dispatch function available anywhere without passing anything down the component tree.

export const App = () => {
  return (
    <TodosContextProvider>
      <MainApp />
    </TodosContextProvider>
  );
};

Using the useContext hook from React within the MainApp function component, you can access values from the TodosContext. In the same component, you can dispatch the action to add more todos.

import { useContext, useState } from 'react';

export const MainApp = () => {
  const [state, dispatch] = useContext(TodosContext);

  // the below is a local state variable to get the item name
  const [item, setItem] = useState('');

  const addTodos = event => {
    if (name !== '') {
      dispatch({
        type: 'ADD_EXPENSE',
        payload: { id: uuid(), item }
      });

      // clean input fields
      setItem('');
    }
  };

  // ... rest of the component code
};

Zustand

Zustand is an open-source state management library that uses the same flux principles but in a simplified manner. It is one of the lightest management libraries available with bundle size at 2kB according to bundlephobia when minified.

It tackles a few of the common pitfalls like:

It is straightforward to get started with Zustand when it comes to creating a store using the hook create method provided by the library. It accepts a callback function as its first argument. The store created is a React hook.

import create from 'zustand';

const useStore = create(set => ({
  todos: [
    {
      id: 1,
      item: 'Buy a notebook'
    },
    {
      id: 2,
      item: 'Buy coffee beans'
    }
  ],
  addTodo: todo =>
    set(state => ({
      todos: [{ item: pokemon.item, id: uuid() }, ...state.todos]
    }))
}));

The callback function accepts a set function as its argument that is used when manipulating the state.

The create method returns the useStore as a hook such that you can use it in any of the React components.

const App = () => {
  const todos = useStore(state => state.todos);

  // ...
};

Zustand also recommends a practice of memoizing selectors with a useCallback hook from React. This prevents unnecessary computations on each render. This way, the React application is optimized for performance in concurrent mode.

Due to its unopinionated nature, Zustand is a great lightweight solution for state management in a React application. It has less boilerplate code and doesn't wrap the React app in context providers. You can create multiple stores to handle different states and actions. It is also compatible with well-known tools like Redux Devtools and offers built-in middleware such as persist to persist the state of the application.

Jotai

Jotai is another open-source state management library developed and maintained by the same team behind Zustand. While the latter is based on flux principles, the former is based on the atomic approach. The state in an application, when split into atoms, is much smaller and lighter when compared to a redux store.

According to bundlephobia, the bundle size at 8.1kB when minified.

For all purposes, the Jotai has a simple API that revolves around Provider, atom, and useAtom hook. An atom is stored inside the React state tree. It uses React Context, so you will have to take care of optimizing the re-rendering of components.

Here is an example of creating a primitive atom with initial values:

import { atom } from 'jotai';

const countAtom = atom(0);
const countryAtom = atom('Japan');
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka']);

Using the atom in a React component is equivalent to using React.useState.

import { useAtom } from 'jotai';

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
    </h1>
  );
}

Jotai offers a hook called useAtomDevTools that can be used to manage Redux DevTools integration for a particular atom.

Valtio

Valtio is a lightweight proxy based state management system for React applications. Proxy is a design pattern for observing an object and modifying it, with each object can have its own custom behavior.

Creating a state with Valtio means you have to wrap the state object in the React app with proxy. Any updates to the state object are handled by the library.

import { proxy } from 'valtio';

const state = proxy({
  count: 0
});

Since the library handles tracking of the updates to the state object, you can mutate it directly in a React component, just like a normal JavaScript object.

setInterval(() => {
  ++state.count;
}, 1000);

Valtio has a built-in hook called useProxy that reads a value from snapshots. Using it, the React component will only re-render when the part of the state in the component changes.

const Counter = () => {
  const snapshot = useProxy(state);
  return (
    <div>
      {snapshot.count}
      <button onClick={() => ++state.count}>Add</button>
    </div>
  );
};

According to bundlephobia, the bundle size at 6.7kB when minified.

Conclusion

Picking up a state management library that fulfills the need of having a centralized store in your React application comes with a variety of options. We went through some of those options in this post. Some of these options are powerful, and while the vanilla React hooks way of dedicating a component's state and not creating a centralized store for everything is a tried and tested approach.