Redux Toolkit with TypeScript

A modern implementation of Redux Toolkit with TypeScript, including async thunks and proper typing


Modern state management with Redux Toolkit and TypeScript, including async thunks, proper typing, and best practices.

Types

types.ts
type User = {
  id: string;
  name: string;
  email: string;
  role: 'USER' | 'ADMIN';
};
 
type UserState = {
  users: User[];
  loading: boolean;
  error: string | null;
  pagination: {
    currentPage: number;
    totalPages: number;
    totalItems: number;
  };
};

Initial State

initialState.ts
const initialState: UserState = {
  users: [],
  loading: false,
  error: null,
  pagination: {
    currentPage: 1,
    totalPages: 1,
    totalItems: 0,
  },
};

Async Thunks

thunks.ts
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async ({ page = 1, limit = 10 }: { page: number; limit: number }) => {
    const response = await axios.get(`/api/users?page=${page}&limit=${limit}`);
    return response.data;
  }
);
 
export const createUser = createAsyncThunk(
  'users/createUser',
  async (userData: Omit<User, 'id'>) => {
    const response = await axios.post('/api/users', userData);
    return response.data;
  }
);

Slice

slice.ts
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    setCurrentPage: (state, action: PayloadAction<number>) => {
      state.pagination.currentPage = action.payload;
    },
    clearError: (state) => {
      state.error = null;
    },
  },
  extraReducers: (builder) => {
    // Fetch users
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload.data;
        state.pagination = {
          currentPage: action.payload.pagination.page,
          totalPages: action.payload.pagination.totalPages,
          totalItems: action.payload.pagination.total,
        };
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch users';
      })
      // Create user
      .addCase(createUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(createUser.fulfilled, (state, action) => {
        state.loading = false;
        state.users.unshift(action.payload);
        state.pagination.totalItems += 1;
        state.pagination.totalPages = Math.ceil(
          state.pagination.totalItems / 10
        );
      })
      .addCase(createUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to create user';
      });
  },
});

Selectors

selectors.ts
export const selectUsers = (state: RootState) => state.users.users;
export const selectLoading = (state: RootState) => state.users.loading;
export const selectError = (state: RootState) => state.users.error;
export const selectPagination = (state: RootState) => state.users.pagination;

Usage Example

UsersList.tsx
function UsersList() {
  const dispatch = useAppDispatch();
  const users = useAppSelector(selectUsers);
  const loading = useAppSelector(selectLoading);
  const error = useAppSelector(selectError);
  const pagination = useAppSelector(selectPagination);
 
  useEffect(() => {
    dispatch(fetchUsers({ page: pagination.currentPage }));
  }, [dispatch, pagination.currentPage]);
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
 
  return (
    <div>
      <div className="grid gap-4">
        {users.map((user) => (
          <div key={user.id} className="p-4 border rounded">
            <h3>{user.name}</h3>
            <p>{user.email}</p>
            <span className="text-sm text-gray-500">{user.role}</span>
          </div>
        ))}
      </div>
 
      <div className="mt-4 flex justify-between">
        <button
          onClick={() => dispatch(setCurrentPage(pagination.currentPage - 1))}
          disabled={pagination.currentPage === 1}
        >
          Previous
        </button>
        <span>
          Page {pagination.currentPage} of {pagination.totalPages}
        </span>
        <button
          onClick={() => dispatch(setCurrentPage(pagination.currentPage + 1))}
          disabled={pagination.currentPage === pagination.totalPages}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Imports

imports.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';

Features:

  • Type-safe Redux implementation
  • Async thunks for API calls
  • Proper error handling
  • Pagination support
  • Optimistic updates
  • Clean action creators
  • Reusable selectors
  • Modern Redux patterns