Modernizing Your React Applications: From Redux to Zustand, TanStack Query, and Redux Toolkit

Makepath recently attended the React US Summit at the Liberty Science Center outside New York City which was an awe-inspiring experience with presenters sharing their work under the grand planetarium dome. The conference provided an opportunity to chat with other React developers and learn about modern state management in React. The following post is intended to give a brief survey of modern react state management solutions and will be particularly useful to people interested in modernizing legacy react / redux applications.

React has come far from the early days, especially in the area of state management.  If you have an application which still uses setState(), this post is for you. If you’re maintaining a React application built five or six years ago using traditional Redux, now is an opportune time to consider updating your state management approach. With the upcoming release of React 19, there’s no better moment to modernize your codebase.

The Evolution of State Management in React

State management is the heartbeat of any React application. It’s what allows your UI components to reflect changes seamlessly, providing a dynamic and responsive user experience. Historically, Redux was the de facto choice for managing state in React apps. While powerful, traditional Redux often came with a steep learning curve and verbose boilerplate code.

Challenges with Traditional Redux

  • Boilerplate Overload: Setting up actions, reducers, and stores can be time-consuming.
  • Complexity: Middleware and async actions introduce additional layers of complexity.
  • Steep Learning Curve: New developers may find it daunting to navigate the intricacies of Redux.

Modern State Management Solutions

The React ecosystem has introduced several libraries that simplify state management, reduce boilerplate, and improve developer experience. Let’s explore three modern alternatives: Zustand, TanStack Query, and Redux Toolkit.


1. Zustand

Zustand is a small, fast, and scalable state management library. It’s minimalistic and doesn’t force a specific structure upon your app.

Key Features:

  • Simplicity: Minimal API and zero boilerplate.
  • Performance: Uses shallow comparison to minimize re-renders.
  • Scalability: Suitable for small and large applications alike.

Example Usage: Counter Application

JavaScript
// store.js
import create from 'zustand';
export const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
}));
JavaScript
// Counter.js
import React from 'react';
import { useStore } from './store';
function Counter() {
  const { count, increment, decrement } = useStore();
  return (
    <div>
      <h2>Zustand Counter</h2>
      <button onClick={decrement}>-</button>
      <span>{` Count: ${count} `}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}
export default Counter;

Why Choose Zustand Over Traditional Redux:

  • Reduces setup time with an intuitive API.
  • Eliminates the need for reducers and action types.
  • Enhances code readability and maintainability.

2. TanStack Query

Formerly known as React Query, TanStack Query specializes in managing server state, offering tools for fetching, caching, and updating data. While it’s primarily used for data fetching, we can utilize it to synchronize state across components.

Key Features:

  • Data Synchronization: Keeps your UI in sync with server or shared state.
  • Automatic Caching: Reduces unnecessary network requests.
  • Background Updates: Refreshes data without interrupting the user experience.

Example Usage: Counter Application

Assuming we have an API that provides the current count value and endpoints to increment and decrement the count.

JavaScript
// api.js
export const fetchCount = async () => {
  const response = await fetch('/api/count');
  const data = await response.json();
  return data.count;
};
export const updateCount = async (newCount) => {
  await fetch('/api/count', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ count: newCount }),
  });
};
JavaScript
// Counter.js
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchCount, updateCount } from './api';
function Counter() {
  const queryClient = useQueryClient();
  const { data: count = 0, isLoading } = useQuery(['count'], fetchCount);
  const mutation = useMutation(updateCount, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries(['count']);
    },
  });
  if (isLoading) return <div>Loading...</div>;
  return (
    <div>
      <h2>TanStack Query Counter</h2>
      <button onClick={() => mutation.mutate(count - 1)}>-</button>
      <span>{` Count: ${count} `}</span>
      <button onClick={() => mutation.mutate(count + 1)}>+</button>
    </div>
  );
}
export default Counter;

Advantages Over Traditional Redux:

  • Simplifies data fetching and synchronization logic.
  • Automatically handles caching and background updates.
  • Reduces the need for additional libraries like Redux Thunk or Saga.

3. Redux Toolkit

Redux Toolkit is the official, opinionated way to write Redux logic. It addresses the common concerns of Redux by eliminating boilerplate and enforcing best practices.

Key Features:

  • Simplified Configuration: Includes utilities to simplify store setup.
  • Immutable Updates: Uses Immer under the hood to allow mutative syntax.
  • Integrated Middleware: Supports async logic with createAsyncThunk.

Example Usage: Counter Application

JavaScript
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    incremented: state => { state.value += 1; },
    decremented: state => { state.value -= 1; },
  },
});
export const { incremented, decremented } = counterSlice.actions;
export default counterSlice.reducer;
JavaScript
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});
JavaScript
// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { incremented, decremented } from './counterSlice';
function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <h2>Redux Toolkit Counter</h2>
      <button onClick={() => dispatch(decremented())}>-</button>
      <span>{` Count: ${count} `}</span>
      <button onClick={() => dispatch(incremented())}>+</button>
    </div>
  );
}
export default Counter;

Improvements Over Traditional Redux:

  • Reduces boilerplate with concise syntax.
  • Encourages the use of modern JavaScript features.
  • Streamlines async operations and side effects.

Why Update Your Legacy React Application Now

With React 19 on the horizon, introducing performance improvements and new features, it’s an ideal time to revisit your application’s architecture.

Benefits of Modernizing:

  • Enhanced Performance: Leverage optimizations in newer React versions.
  • Improved Developer Experience: Simplify codebases and reduce technical debt.
  • Future-Proofing: Stay ahead with the latest best practices and community standards.

What to Expect in React 19:

  • Potential new features that may deprecate old patterns.
  • Improved concurrency and rendering optimizations.
  • Better support for modern state management libraries.

Migration Strategies

1. Gradual Adoption:

  • Start by introducing modern libraries in new features or modules.
  • Run both old and new state management systems side by side temporarily.

2. Component Refactoring:

  • Identify components tightly coupled with old Redux patterns.
  • Refactor these components to use context or modern libraries.

3. Team Training:

  • Educate your development team on new libraries and best practices.
  • Share resources and conduct code reviews to ensure quality.

Common Pitfalls to Avoid:

  • Overcomplicating the Migration: Keep changes incremental to manage risk.
  • Ignoring Testing: Ensure you have robust tests to catch regressions.
  • Neglecting Documentation: Update your documentation to reflect new patterns.

Conclusion

Updating your legacy React application is not just about keeping up with trends; it’s about enhancing performance, maintainability, and developer happiness. Modern state management libraries like Zustand, TanStack Query, and Redux Toolkit offer streamlined solutions that can breathe new life into your applications.


Ready to Modernize Your React Application?

At Makepath, we specialize in modernizing legacy React applications. Our team has the expertise to seamlessly transition your app to the latest state management solutions, ensuring improved performance and a better developer experience.

Contact us today to discuss how we can help you update your application in time for React 19 and beyond.