Redux & Redux Toolkit
Khái niệm Cơ bản
Redux là state management library theo unidirectional data flow:
┌─────────────────────────────────────────────────────────────┐
│ Redux Data Flow │
│ │
│ Component ──dispatch(action)──▶ Reducer ──▶ Store │
│ ▲ │ │
│ └──────────────── state ───────────────────────────┘ │
│ │
│ Action: { type: 'counter/increment', payload: 1 } │
│ Reducer: Pure function (state, action) => newState │
│ Store: Single source of truth │
└─────────────────────────────────────────────────────────────┘
Redux Toolkit (RTK) - Cách hiện đại
RTK là cách chính thức và được khuyến nghị để dùng Redux.
Cài đặt
npm install @reduxjs/toolkit react-redux
createSlice
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// RTK dùng Immer - có thể "mutate" state trực tiếp
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
},
},
});
// Export actions
export const { increment, decrement, incrementByAmount, reset } =
counterSlice.actions;
// Export selectors
export const selectCount = (state: RootState) => state.counter.value;
export default counterSlice.reducer;
configureStore
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import cartReducer from '../features/cart/cartSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
cart: cartReducer,
},
// Middleware mặc định: thunk, serializability check
// middleware: (getDefaultMiddleware) =>
// getDefaultMiddleware().concat(myMiddleware),
});
// TypeScript types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Provider Setup
// index.tsx / main.tsx
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>
);
Sử dụng trong Component
import { useAppDispatch, useAppSelector } from '../../app/store';
import { increment, decrement, selectCount } from './counterSlice';
function Counter() {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
createAsyncThunk - Async Operations
// features/users/usersSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
email: string;
}
interface UsersState {
entities: User[];
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
// Tạo async thunk
export const fetchUsers = createAsyncThunk(
'users/fetchAll',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Server error');
return await response.json() as User[];
} catch (error) {
return rejectWithValue((error as Error).message);
}
}
);
export const createUser = createAsyncThunk(
'users/create',
async (userData: Omit<User, 'id'>, { rejectWithValue }) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) throw new Error('Failed to create user');
return await response.json() as User;
} catch (error) {
return rejectWithValue((error as Error).message);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
status: 'idle',
error: null,
} as UsersState,
reducers: {
userRemoved: (state, action: PayloadAction<number>) => {
state.entities = state.entities.filter((u) => u.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
// fetchUsers
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
})
// createUser
.addCase(createUser.fulfilled, (state, action) => {
state.entities.push(action.payload);
});
},
});
export const { userRemoved } = usersSlice.actions;
export default usersSlice.reducer;
// Selectors
export const selectAllUsers = (state: RootState) => state.users.entities;
export const selectUsersStatus = (state: RootState) => state.users.status;
// Component sử dụng
function UserList() {
const dispatch = useAppDispatch();
const users = useAppSelector(selectAllUsers);
const status = useAppSelector(selectUsersStatus);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [dispatch, status]);
if (status === 'loading') return <Spinner />;
if (status === 'failed') return <p>Error loading users</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => dispatch(userRemoved(user.id))}>Delete</button>
</li>
))}
</ul>
);
}
RTK Query - Data Fetching
RTK Query là giải pháp data fetching tích hợp trong Redux Toolkit.
// services/usersApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['User'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User'],
}),
getUserById: builder.query<User, number>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
createUser: builder.mutation<User, Omit<User, 'id'>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['User'], // Tự động refetch users sau khi create
}),
updateUser: builder.mutation<User, Partial<User> & { id: number }>({
query: ({ id, ...body }) => ({
url: `/users/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
deleteUser: builder.mutation<void, number>({
query: (id) => ({ url: `/users/${id}`, method: 'DELETE' }),
invalidatesTags: ['User'],
}),
}),
});
// Export hooks tự động generate
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useCreateUserMutation,
useUpdateUserMutation,
useDeleteUserMutation,
} = usersApi;
// Thêm vào store
export const store = configureStore({
reducer: {
[usersApi.reducerPath]: usersApi.reducer,
// ...other reducers
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(usersApi.middleware),
});
// Sử dụng RTK Query hooks
function UserList() {
const { data: users = [], isLoading, isError } = useGetUsersQuery();
const [deleteUser] = useDeleteUserMutation();
if (isLoading) return <Spinner />;
if (isError) return <p>Error!</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteUser(user.id)}>Delete</button>
</li>
))}
</ul>
);
}
// Conditional fetching
function UserProfile({ userId }: { userId: number | null }) {
const { data: user } = useGetUserByIdQuery(userId!, {
skip: userId === null, // Không fetch nếu userId null
});
return <div>{user?.name}</div>;
}