DEV Community

Mohammed Dawood
Mohammed Dawood

Posted on

Mastering Global State Management in React 19 with TypeScript: Redux Toolkit, Redux Thunk, Recoil, and Zustand

Redux vs Recoil vs Zustand

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

  1. Install Dependencies
   npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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')
   );
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

Handling Asynchronous Requests with Redux Thunk

Redux Thunk middleware enables handling asynchronous logic in Redux.

  1. Install Redux Thunk
   npm install redux-thunk
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

2. Recoil

Recoil provides a flexible and scalable state management solution with atoms and selectors.

Setting Up Recoil

  1. Install Recoil
   npm install recoil
Enter fullscreen mode Exit fullscreen mode
  1. 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,
   });
Enter fullscreen mode Exit fullscreen mode
   // 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
     },
   });
Enter fullscreen mode Exit fullscreen mode
  1. 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')
   );
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

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;
  },
});
Enter fullscreen mode Exit fullscreen mode

3. Zustand

Zustand offers a minimalistic and scalable approach to state management with hooks.

Setting Up Zustand

  1. Install Zustand
   npm install zustand
Enter fullscreen mode Exit fullscreen mode
  1. 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 });
     },
   }));
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)