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:
-
React Context via the
useContext
hook - Zustand
- Redux via ReduxToolkit
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>
);
};
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>
);
};
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>
);
}
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.
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 })),
}));
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>
);
};
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;
We now have two stores available that behave exactly the same way
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,
},
});
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>
);
};
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
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.
Only 3kb minified. That’s lightweight!
ReduxToolkit: For this to work we need react-redux
and @redux/toolkit
.
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)
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:
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.