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
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
const initialState: UserState = {
users: [],
loading: false,
error: null,
pagination: {
currentPage: 1,
totalPages: 1,
totalItems: 0,
},
};Async Thunks
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
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
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
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
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