Let's get our hands dirty and directly dive in with a simple example.
For an illustration purpose, let's assume global counter state (just for simplicity) is going to be implemented.
Redux way:
First list the actions:
action.js
export const actionTypes = {
incCounter: "INCREASE_COUNTER",
decCounter: "DECREASE_COUNTER",
setCounter: "SET_COUNTER"
};
Then let's create the saga and do the all the generator function and yield gymnastics 😅
saga.js
import { takeLatest, put, all } from "redux-saga/effects";
import { actionTypes } from "./action.js"
function* increaseCounter() {
yield put({ type: actionTypes.incCounter });
}
function* decreaseCounter() {
yield put({ type: actionTypes.decCounter });
}
function* setCounter(action) {
yield put({ type: actionTypes.setCounter, value: action.value });
}
export function* rootSaga() {
yield all([
takeLatest(actionTypes.incCounter, increaseCounter),
takeLatest(actionTypes.decCounter, decreaseCounter),
takeLatest(actionTypes.setCounter, setCounter),
]);
}
export default rootSaga;
Now, let's modify the state in the reducer:
reducer.js
import { actionTypes } from "./action.js"
const initialState = {
count: 0
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.incCounter:
const { count } = state;
return { ...state, count: count++ }
break;
case actionTypes.decCounter:
const { count } = state;
return { ...state, count: count-- }
break;
case actionTypes.setCounter:
return { ...state, count: action.value }
break;
default:
return state
break;
}
};
export default rootReducer;
We need to wrap the whole application (or part of it) with redux store. I know it's a one-time process but looks ugly indeed.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./reducers/rootReducer";
import { rootSaga } from "./sagas/saga";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
Finally, let's also cover the use case in a component (selector and dispatch gymnastics 😅)
component.js
import { useSelector, useDispatch } from "react-redux";
import { actionTypes } from "./action.js"
const Component = () => {
// using redux state
const count = useSelector(state => state.count);
// dispatching actions
const dispatch = useDispatch();
const handleIncreaseCounter = () => {
dispatch({ type: actionTypes.incCounter })
}
const handleDecreaseCounter = () => {
dispatch({ type: actionTypes.decCounter })
}
const handleSetCounter = (value) => {
dispatch({ type: actionTypes.setCounter, value })
}
// ...
}
Isn't these too much for a simple global state with some actions? Yes I know some of these stuff like creating middleware and wrapping redux provider store is a one time job but still those saga.js and reducer.js files are alone too much.
Okay, let's see Zustand's way:
Let's setup. Let's even use TypeScript to increase line number 🤣
use-counter.ts
import { create } from "zustand";
type CounterStore = {
count: number;
increase: () => void;
decrease: () => void;
setCounter: (count: number) => void;
};
export const useCounter = create<CounterStore>((set, get) => ({
count: 0,
increase: () => set({ count: get().count++ }),
decrease: () => set({ count: get().count-- }),
setCounter: (count: number) => set({ count }),
}));
That's it. This file alone does the jobs of the four files (action.js, saga.js, reducer.js and the Redux provider steps in the index.js) above.
Let's cover the use case in a component for the sake of completeness:
component.js
import { useCounter } from "./use-counter";
const Component = () => {
const { count, increase, decrease, setCounter } = useCounter();
const handleIncreaseCounter = () => increase();
const handleDecreaseCounter = () => decrease();
const handleSetCounter = (value) => setCounter(value);
//...
}
It's a blessing for state managing ✨
I'm not going to argue against the fact that both redux and zustand are great libraries with their use cases.
But coming from redux, I decided to try zustand for fun - and I was blown away by how straightforward it is. Having used zustand for a while, I felt depressed thinking about the time I spent boilerplating in redux, debugging actions, side effects...
In zustand, everything works intuitively. You're not importing hundreds of actions everywhere. You're not boilerplating yourself into oblivion. You're not writing up circular dependencies like it's a circus. You don't end up depending on weird redux specifics.
Even though zustand is minimalistic and unopinionated compared to redux, I would still consider it easier to use than redux. Redux is a paradigm in itself that requires some understanding of underlying concepts. Zustand seems more like a toolkit that understands very well what it tries to do (manage state).
Overall I think both are fine. After having used zustand, I'm not going to use redux if I can decide, because redux is literally twice or triple the work. If I care a lot about maintainability, I would probably go with zustand too.
Thank you for reading this far. See you in the next chapter :)