Mastering Global State Management in React 19 with TypeScript: Redux Toolkit, Redux Thunk, Recoil, and Zustand
Mohammed Dawood
Posted on August 22, 2024
Managing the global state effectively is essential for building scalable and maintainable React applications. In this guide, we’ll explore four popular state management solutions—Redux Toolkit, Redux Thunk, Recoil, and Zustand—and show how to handle asynchronous requests with these tools, using TypeScript for type safety.
1. Redux Toolkit
Redux Toolkit simplifies Redux setup and management with best practices built-in. It’s an ideal choice for complex state management.
Setting Up Redux Toolkit
- Install Dependencies
npm install @reduxjs/toolkit react-redux
- Create a Redux Slice
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
- Configure the Store
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
- Provide the Store
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- Use Redux State in Components
// features/counter/Counter.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
import { RootState, AppDispatch } from '../app/store';
const Counter: React.FC = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
};
export default Counter;
Handling Asynchronous Requests with Redux Thunk
Redux Thunk middleware enables handling asynchronous logic in Redux.
- Install Redux Thunk
npm install redux-thunk
- Configure Redux Store with Thunk
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
middleware: [thunk],
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
- Create an Async Action
// features/counter/counterSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
export const fetchData = createAsyncThunk('counter/fetchData', async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
});
interface CounterState {
value: number;
data: any[];
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
const initialState: CounterState = {
value: 0,
data: [],
status: 'idle',
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchData.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchData.fulfilled, (state, action: PayloadAction<any[]>) => {
state.status = 'succeeded';
state.data = action.payload;
})
.addCase(fetchData.rejected, (state) => {
state.status = 'failed';
});
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
2. Recoil
Recoil provides a flexible and scalable state management solution with atoms and selectors.
Setting Up Recoil
- Install Recoil
npm install recoil
- Create Atoms and Selectors
Atoms represent state, while selectors compute derived state.
// atoms/counterAtom.ts
import { atom } from 'recoil';
export const counterAtom = atom<number>({
key: 'counterAtom',
default: 0,
});
// selectors/counterSelector.ts
import { selector } from 'recoil';
import { counterAtom } from '../atoms/counterAtom';
export const counterSelector = selector<number>({
key: 'counterSelector',
get: ({ get }) => {
const count = get(counterAtom);
return count * 2; // Example transformation
},
});
- Provide RecoilRoot
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root')
);
- Use Recoil State in Components
// components/Counter.tsx
import React from 'react';
import { useRecoilState } from 'recoil';
import { counterAtom } from '../atoms/counterAtom';
const Counter: React.FC = () => {
const [count, setCount] = useRecoilState(counterAtom);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
};
export default Counter;
Handling Asynchronous Requests with Recoil
Recoil’s selectors can manage asynchronous data fetching.
// selectors/fetchDataSelector.ts
import { selector } from 'recoil';
export const fetchDataSelector = selector<any[]>({
key: 'fetchDataSelector',
get: async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
},
});
3. Zustand
Zustand offers a minimalistic and scalable approach to state management with hooks.
Setting Up Zustand
- Install Zustand
npm install zustand
- Create a Store
// store.ts
import create from 'zustand';
interface StoreState {
count: number;
increment: () => void;
decrement: () => void;
fetchData: () => Promise<void>;
data: any[];
}
export const useStore = create<StoreState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
data: [],
fetchData: async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
set({ data });
},
}));
- Use the Store in Components
// components/Counter.tsx
import React from 'react';
import { useStore } from '../store';
const Counter: React.FC = () => {
const { count, increment, decrement } = useStore();
return (
<div>
<p>{count}</p>
<button onClick={() => increment()}>Increment</button>
<button onClick
={() => decrement()}>Decrement</button>
</div>
);
};
export default Counter;
Handling Asynchronous Requests with Zustand
Asynchronous operations are handled directly within the store.
// components/DataFetcher.tsx
import React, { useEffect } from 'react';
import { useStore } from '../store';
const DataFetcher: React.FC = () => {
const { fetchData, data } = useStore();
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
<h2>Data:</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default DataFetcher;
Conclusion
Choosing the right state management solution depends on your application’s needs and complexity. Redux Toolkit offers a robust solution with built-in best practices, Recoil provides flexibility with atoms and selectors, and Zustand offers a minimalistic approach. Handling asynchronous requests is straightforward in each of these solutions, with Redux Thunk providing middleware for async actions, Recoil selectors managing async queries, and Zustand integrating async operations directly into the store. Using TypeScript adds type safety, ensuring a more reliable and maintainable codebase.
Happy coding!
Posted on August 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.