DEV Community

Cover image for I tried React Context, Redux and Zustand stores in the same app
Fernando González Tostado
Fernando González Tostado

Posted on • Edited on

I tried React Context, Redux and Zustand stores in the same app

Me and my team are very used to working with redux since we usually work with big scalable apps that require a complex state management tool — where redux comes super handy.

However, one day we had to create a simple web page to display some specific information about a product that we were working on. This app would be a quite simple app that might get eventually bigger, therefore it might not require a complex state management option.

That put us in the situation where we wanted to think in ˆ management options, and not only Redux. Which works perfectly, but hey, what a great chance to learn a bit more of other alternatives!

We thought of three options for this exercise:

Let’s create a brand new CRA and add these three options so we can see how different the setup would be.

For the sake of simplicity all these stores will be a contrived example with a counter, increment and decrement actions. When the app gets bigger, good organization practices should be considered for correct scalability.

The example is based on a create-react-app project.

We’ll start with withe React Context via useContext, since it’s a hook that comes already with React and should be simpler to understand if you have not used either of the other two incoming alternatives.

First, create a store

// scr/stores/context/store.js

import React, { useState } from "react";

export const CounterContext = React.createContext();

export const CounterContextProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </CounterContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, we used useState to store the state with the counter . For a more advanced —and close to how redux works— state management there's useReducer (more info).

Then, create a Context component that will display and change the counter value


// src/components/Context.js

import { useContext } from "react";
import { CounterContext } from "../stores/context/store";

export const Context = () => {
  const { count, increment, decrement } = useContext(CounterContext);

  return (
    <div className="context">
      <h1>Context Container</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <p>Counter: {count}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

We access to the value prop elements from the store via useContext(CounterContext)

Finally, add it to App.js


// src/App.js

import { CounterContextProvider } from "./stores/context/store";
import { Context } from "./components/Context";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <CounterContextProvider>
          <Context />
        </CounterContextProvider>
      </header>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note that the CounterContextProvider must wrap the Context component in order to make the children have access to the values from it.

And we’ve got it working. Easy. Kind of.

context

Now it comes Zustand. For a lot of people it is a halfway between the simplicity of useContext and the apparent complexity of Redux.

The zustand store

// src/stores/zustand/store.js

import create from "zustand";

export const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
Enter fullscreen mode Exit fullscreen mode

Have you noticed how this one was remarkably simpler than the useContext wrapper function?

Now the component Zustand

// src/components/Zustand.js

import { useCounterStore } from "../stores/zustand/store";

export const Zustand = () => {
  const counter = useCounterStore((state) => state.count);
  const increaseCount = useCounterStore((state) => state.increment);
  const decreaseCount = useCounterStore((state) => state.decrement);

  return (
    <div className="zustand">
      <h1>Zustand Container</h1>
      <button onClick={increaseCount}>Increment</button>
      <button onClick={decreaseCount}>Decrement</button>
      <p>Counter: {counter}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here the different values/methods should be declared one by one via with the useCounterStore .

And finally, we added the Zustand component in App.js . We don’t need to wrap anything. The component doesn't require any Provider as Context or Redux would, that's cool.

// src/App.js

import { Zustand } from './components/Zustand';
import { CounterContextProvider } from './stores/context/store';
import { Context } from './components/Context';

function App() {
  return (
    <div className='App'>
      <header className='App-header'>
        // no wrapper needed!
        <Zustand />
        <CounterContextProvider>
          <Context />
        </CounterContextProvider>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

We now have two stores available that behave exactly the same way

zustand-result

And finally comes Redux, via ReduxToolKit.

First step would be creating the store

// src/stores/redux-tool-kit/slices/counter.js

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

// src/stores/redux-tool-kit/store.js

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./slices/counter";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
Enter fullscreen mode Exit fullscreen mode

This time the store requires two steps, the configuration via the configureStore method and the slice via the createSlice method that then goes into the reducer object from the store.

the ReduxToolkit component looks like this:

// src/components/ReduxToolKit.js
import { useDispatch, useSelector } from "react-redux";
import { decrement, increment } from "../stores/redux-tool-kit/slices/counter";

export const ReduxToolKit = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter.value);
  const increase = () => dispatch(increment());
  const decrease = () => dispatch(decrement());

  return (
    <div className="redux-toolkit">
      <h1>Redux Tool Kit Container</h1>
      <button onClick={increase}>Increment</button>
      <button onClick={decrease}>Decrement</button>
      <p>Counter: {counter}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Note how we had to declare a dispatch method from useDispatch that takes as argument the actual action methods. Besides that, it looks quite similar to the Zustand functional component.

And finally, we have to wrap the ReduxToolKit component in the App component with the Provider component from react-redux and pass it to the store prop. Similar to what we did with the Context store passing the value prop.

And if we check it in the UI, it behaves exactly as the other components

redux-1

Extra: Bundle size
Ok, now, what about the size of each of these three options? How would it affect our bundle size in production? Let’s check the three scenarios:

Context is obviously 0, it comes with React, so it doesn’t affect our bundle size.

Zustand. This is what bundlephobia shows us about it.

zustand-1

Only 3kb minified. That’s lightweight!

ReduxToolkit: For this to work we need react-redux and @redux/toolkit.

phobia-1

phobia-2

This one is a bit heavier ~55kb minified, but nothing that should worry us.

If you want to understand a bit more about the cost of the packages weight, this is a nice article.

Our decision
For the first mentioned app where we only required a simple state management system we went with Zustand. Only because it’s super straightforward and we didn’t want to over complicate something that didn’t require it. But either of them would have been a perfectly fine option. Also, it helped me to understand a bit better Zustand which was completely new to me.

Conclusion
I personally consider that RTK made it super easy to better understand the redux-pattern, and reduced a lot of boilerplate for the initial setup. Any middle-senior developer should be able to use it without much problems. Besides, the advanced use cases with thunks, middleware, immutability with immer, Redux DevTools (these are awesome for debugging) are life savers. Once you are familiar with its use you will find it hard to go back to other ‘simpler’ alternatives.

Depending on the characteristics of a project — complexity, size, architecture — any of these options are very viable. Even Context, which — honestly — I still don’t enjoy using.

Check the code with the code if you want more specifics for the code of this article.

Initially posted in Medium

Top comments (2)

Collapse
 
azariaberyl profile image
Azaria Beryl

Hello, this is a great article for me to start learning about third-party state management libraries. I've tried RTK before, but it was kinda hard for me xD, so I decided to learn Zustand since it's similar to React Context. I think there have been some changes within Zustand since this post was written.

However, I have several questions for you, if you don't mind:

  1. Have you tried Zustand again?
  2. What do you think about using Zustand in medium or large scale apps?
Collapse
 
esponges profile image
Fernando González Tostado

Hey, thanks for your response.

No, I've not used Zustand for medium or large scale apps. I've only used it in smaller apps, and not recently to be honest, but I don't think that the core idea has changed a lot which is the simplicity.

What I've used in larger apps is Redux, and believe me, most of them will use it, so you'd probably want to give it again a shot, it's a bit harder at the beginning but very worth the effort.