Bài 21.1: Cài đặt Redux với TypeScript

Bài 21.1: Cài đặt Redux với TypeScript
Chào mừng bạn đến với Bài 21.1 trong series Lập trình Web Front-end của chúng ta! Hôm nay, chúng ta sẽ cùng nhau bước chân vào thế giới của việc quản lý state (trạng thái) trong các ứng dụng React lớn hơn. Khi ứng dụng của bạn phát triển, việc truyền dữ liệu giữa các component ngày càng trở nên phức tạp (prop drilling là một nỗi ám ảnh!). Đây chính là lúc các thư viện quản lý state như Redux tỏa sáng.
Và khi kết hợp Redux với sức mạnh của TypeScript, bạn không chỉ giải quyết được vấn đề quản lý state tập trung mà còn đảm bảo rằng state và các hành động của bạn luôn có kiểu dữ liệu rõ ràng, giúp giảm thiểu lỗi và làm cho code dễ đọc, dễ bảo trì hơn rất nhiều.
Bài viết này sẽ hướng dẫn bạn từng bước cài đặt và cấu hình Redux với TypeScript trong một ứng dụng React hiện đại, sử dụng thư viện được khuyến nghị hiện nay là Redux Toolkit. Hãy cùng bắt đầu nhé!
Tại sao lại là Redux Toolkit?
Trước đây, việc cài đặt Redux khá cồng kềnh với nhiều boilerplate code. Tuy nhiên, Redux Toolkit (RTK) đã ra đời để giải quyết vấn đề đó. RTK là bộ công cụ chính thức được Redux khuyến nghị, giúp đơn giản hóa đáng kể quá trình cài đặt và sử dụng Redux. Nó bao gồm các tiện ích giúp:
- Giảm thiểu boilerplate.
- Cấu hình store dễ dàng hơn.
- Viết reducers hiệu quả hơn với
createSlice
. - Tích hợp sẵn các middleware hữu ích (như Redux Thunk để xử lý bất đồng bộ).
- Đặc biệt, nó được thiết kế để hoạt động tuyệt vời với TypeScript ngay từ đầu!
Vì vậy, trong bài viết này, chúng ta sẽ tập trung hoàn toàn vào việc sử dụng Redux Toolkit.
Chuẩn bị
Trước khi bắt đầu, đảm bảo bạn đã có:
- Node.js và npm hoặc yarn đã cài đặt.
- Một dự án React đã được tạo (ví dụ: sử dụng Create React App với template TypeScript hoặc Vite).
- Kiến thức cơ bản về React và TypeScript.
Nếu bạn đang sử dụng Vite, bạn có thể tạo một dự án React + TypeScript nhanh chóng bằng lệnh sau:
npm create vite my-redux-app --template react-ts
# hoặc
yarn create vite my-redux-app --template react-ts
# hoặc
pnpm create vite my-redux-app --template react-ts
Sau đó di chuyển vào thư mục dự án và cài đặt các dependencies:
cd my-redux-app
npm install
# hoặc
yarn install
# hoặc
pnpm install
Bước 1: Cài đặt các Dependencies cần thiết
Chúng ta cần cài đặt Redux Toolkit, thư viện react-redux
để kết nối Redux với React, và các định nghĩa kiểu TypeScript tương ứng.
Mở terminal trong thư mục gốc của dự án và chạy lệnh sau:
npm install @reduxjs/toolkit react-redux
npm install @types/react-redux --save-dev
Giải thích:
@reduxjs/toolkit
: Chứa Redux Toolkit, bộ công cụ chính.react-redux
: Thư viện giúp kết nối React components với Redux store (ví dụ: hooksuseSelector
,useDispatch
).@types/react-redux
: Chứa các định nghĩa kiểu TypeScript choreact-redux
, giúp TypeScript hiểu cách sử dụng các hooks và componentProvider
của thư viện này.
Bước 2: Cấu hình Redux Store
Store là "ngôi nhà" chứa toàn bộ trạng thái của ứng dụng của bạn. Chúng ta sẽ sử dụng configureStore
từ Redux Toolkit để tạo store.
Tạo một thư mục mới, ví dụ src/app
, và bên trong đó tạo file store.ts
.
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
// Chúng ta sẽ thêm các "reducers" (bộ xử lý trạng thái) vào đây sau.
// Mỗi key ở đây sẽ đại diện cho một phần của trạng thái tổng thể.
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
// Infer the `RootState` (Loại trạng thái gốc) và `AppDispatch` (Loại hàm dispatch) từ store
export type RootState = ReturnType<typeof store.getState>;
// Infer the `AppDispatch` type from the store's dispatch function
export type AppDispatch = typeof store.dispatch;
Giải thích:
configureStore
: Một hàm từ Redux Toolkit giúp cấu hình store với ít cài đặt hơn so với Redux truyền thống. Nó tự động thêm middleware (như Redux Thunk) và cài đặt Redux DevTools extension.reducer
: Một object chứa tất cả các slice reducers của bạn. Mỗi key trong object này (ví dụ:counter
,user
,items
) sẽ tương ứng với một phần của trạng thái tổng thể (state.counter
,state.user
,state.items
).export const store
: Xuất store đã tạo để có thể sử dụng ở nơi khác trong ứng dụng.export type RootState = ReturnType<typeof store.getState>;
: Đây là cách quan trọng để TypeScript biết toàn bộ cấu trúc trạng thái của ứng dụng của bạn.ReturnType<typeof store.getState>
sẽ tự động suy ra kiểu của hàmstore.getState
, tức là kiểu của toàn bộ trạng thái gốc sau khi kết hợp tất cả các reducers.export type AppDispatch = typeof store.dispatch;
: Tương tự, dòng này giúp TypeScript biết kiểu của hàmstore.dispatch
. Điều này quan trọng vìconfigureStore
tự động thêm middleware vàodispatch
, và kiểu này sẽ bao gồm cả các middleware đó (ví dụ: cho phép dispatch hàm bất đồng bộ nếu dùng Thunk).
Bước 3: Cung cấp Store cho ứng dụng React
Để các component trong React có thể truy cập vào Redux store, chúng ta cần "cung cấp" store đó bằng component Provider
từ thư viện react-redux
.
Mở file entry point của ứng dụng (thường là src/index.tsx
hoặc src/main.tsx
nếu dùng Vite).
// src/index.tsx (hoặc src/main.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client'; // hoặc 'react-dom'
import App from './App';
import { store } from './app/store'; // Nhập store từ file vừa tạo
import { Provider } from 'react-redux'; // Nhập Provider
// Chọn root element của ứng dụng
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container!); // Tạo root
root.render(
<React.StrictMode>
{/* Bọc component gốc <App /> bằng Provider và truyền store vào prop store */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Giải thích:
Provider
: Component này sử dụng React Context API để đưa store vào "ngữ cảnh" của ứng dụng. Bằng cách đặt nó ở cấp độ cao nhất (thường bọc componentApp
), tất cả các component con bên trongApp
đều có thể truy cập vào store.store={store}
: Truyền instance store mà chúng ta đã tạo ở Bước 2 vào propstore
củaProvider
.
Bước 4: Tạo các Typed Hooks
Để sử dụng useSelector
và useDispatch
(các hooks từ react-redux
để đọc state và gửi action) một cách an toàn với TypeScript, Redux Toolkit khuyến nghị tạo ra các phiên bản hooks đã được typed sẵn. Điều này giúp tránh việc phải gán kiểu thủ công mỗi lần sử dụng hooks và đảm bảo tính nhất quán trong toàn bộ ứng dụng.
Tạo một file mới, ví dụ src/app/hooks.ts
.
// src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store'; // Nhập kiểu RootState và AppDispatch
// Sử dụng các exported hooks này trong toàn bộ ứng dụng thay vì plain `useDispatch` và `useSelector`
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Giải thích:
- Chúng ta nhập
useDispatch
vàuseSelector
từreact-redux
. - Chúng ta nhập
RootState
vàAppDispatch
từ filestore.ts
mà chúng ta đã tạo. Sử dụngimport type
chỉ nhập các khai báo kiểu, không phải giá trị JavaScript, giúp tối ưu hóa bundle size. export const useAppDispatch = () => useDispatch<AppDispatch>();
: Tạo một hook tùy chỉnhuseAppDispatch
. Nó gọiuseDispatch
gốc nhưng chỉ định rõ ràng kiểu trả về làAppDispatch
. Điều này đảm bảo rằng khi bạn sử dụnguseAppDispatch
, TypeScript biết chính xác các loại action bạn có thể dispatch (bao gồm cả các action bất đồng bộ nếu có middleware).export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
: Tạo một hook tùy chỉnhuseAppSelector
. Chúng ta gán kiểuTypedUseSelectorHook<RootState>
cho nó.TypedUseSelectorHook
là một kiểu generic được cung cấp bởireact-redux
, giúp bạn chỉ định kiểu của toàn bộ state mà selector của bạn sẽ nhận vào (ở đây làRootState
). Điều này đảm bảo rằng khi bạn sử dụnguseAppSelector
, TypeScript biết cấu trúc của trạng thái và có thể cung cấp gợi ý code cũng như kiểm tra lỗi kiểu.
Từ bây giờ, bạn sẽ sử dụng useAppDispatch
và useAppSelector
trong các component React của mình thay vì useDispatch
và useSelector
gốc.
Bước 5: Tạo một Redux Slice (Ví dụ: Counter)
Redux Toolkit giới thiệu khái niệm "slice". Một slice là một phần của logic Redux (state, reducers và actions) cho một tính năng cụ thể trong ứng dụng của bạn (ví dụ: slice cho người dùng, slice cho sản phẩm, slice cho bộ đếm).
createSlice
là hàm "ma thuật" từ RTK giúp bạn định nghĩa một slice một cách dễ dàng.
Tạo một thư mục mới, ví dụ src/features
, và bên trong tạo thư mục con cho tính năng của bạn, ví dụ counter
. Trong thư mục counter
, tạo file counterSlice.ts
.
// src/features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Định nghĩa kiểu cho trạng thái của slice này
// Define a type for the slice state
interface CounterState {
value: number;
// Bạn có thể thêm các trường khác vào trạng thái slice của mình
// status: 'idle' | 'loading' | 'failed';
}
// Định nghĩa trạng thái ban đầu sử dụng kiểu đã định nghĩa
// Define the initial state using that type
const initialState: CounterState = {
value: 0,
// status: 'idle',
};
export const counterSlice = createSlice({
name: 'counter', // Tên của slice. Sử dụng trong các action type (ví dụ: 'counter/increment')
initialState, // Trạng thái ban đầu
reducers: {
// Reducers định nghĩa cách trạng thái thay đổi dựa trên các action
// RTK cho phép 'thay đổi' trạng thái trực tiếp nhờ thư viện Immer
increment: (state) => {
// Redux Toolkit cho phép chúng ta viết logic mutating ngay trong reducers.
// Điều này không thực sự thay đổi state gốc mà tạo ra một bản sao mới.
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Action với payload: Nhận dữ liệu bổ sung
incrementByAmount: (state, action: PayloadAction<number>) => {
// `PayloadAction<number>` khai báo rằng `action.payload` sẽ là một số
state.value += action.payload;
},
// Bạn có thể thêm nhiều reducers khác ở đây
},
// extraReducers được sử dụng để xử lý các action types được định nghĩa bên ngoài slice này,
// ví dụ: action từ các slice khác hoặc các async thunks (sẽ học sau).
// extraReducers: (builder) => {
// ...
// },
});
// `createSlice` tự động tạo ra các action creator functions cho mỗi reducer mà chúng ta định nghĩa.
// Ví dụ: action type 'counter/increment' sẽ có action creator `increment()`.
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Hàm selector ví dụ (Tùy chọn nhưng là best practice để truy cập trạng thái slice)
// Bạn có thể định nghĩa các selector ở đây hoặc ngay trong component.
// import type { RootState } from '../../app/store'; // Nhập RootState nếu định nghĩa selector tại đây
// export const selectCounterValue = (state: RootState) => state.counter.value;
// Hàm reducer chính của slice này để thêm vào store
export default counterSlice.reducer;
Giải thích:
interface CounterState
: Chúng ta định nghĩa kiểu dữ liệu cho trạng thái của slicecounter
. Đây là nơi TypeScript phát huy tác dụng, giúp bạn biết rõ cấu trúc dữ liệu mà slice này quản lý.initialState: CounterState
: Khởi tạo trạng thái ban đầu cho slice, đảm bảo nó tuân thủ kiểuCounterState
.createSlice({...})
: Hàm chính để tạo slice.name: 'counter'
: Đặt tên cho slice. RTK sẽ sử dụng tên này để tạo các action type (ví dụ:'counter/increment'
).initialState
: Cung cấp trạng thái ban đầu.reducers
: Một object chứa các hàm reducer. Mỗi hàm ở đây sẽ trở thành một case reducer xử lý một action cụ thể. RTK sử dụng thư viện Immer bên dưới, cho phép bạn "mutation" (thay đổi trực tiếp) trạng thái nháp bên trong reducer một cách an toàn, và Immer sẽ tự động tạo ra một trạng thái mới và bất biến dựa trên các thay đổi đó. Điều này làm cho code reducer dễ đọc và viết hơn rất nhiều.increment: (state) => { ... }
: Reducerincrement
. Nhận trạng thái hiện tại (state
).incrementByAmount: (state, action: PayloadAction<number>) => { ... }
: ReducerincrementByAmount
. Nhận trạng thái và objectaction
.PayloadAction<number>
là kiểu generic từ RTK giúp bạn khai báo kiểu dữ liệu củaaction.payload
. TypeScript giờ đây biết rằngaction.payload
sẽ là một số.
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
:createSlice
tự động tạo ra các action creator (hàm tạo action) cho mỗi reducer. Chúng ta xuất chúng để có thể gọi các hàm này trong component (ví dụ:dispatch(increment())
).export default counterSlice.reducer;
: Xuất reducer chính của slice. Đây là hàm mà chúng ta sẽ thêm vào objectreducer
trongconfigureStore
.
Bước 6: Kết nối Slice với Store
Bây giờ chúng ta đã có slice counter
, hãy thêm reducer của nó vào store tổng thể.
Mở lại file src/app/store.ts
và cập nhật nó:
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice'; // Nhập reducer từ slice
export const store = configureStore({
reducer: {
// Thêm slice reducer vào đây. Key 'counter' sẽ là tên phần trạng thái trong RootState
counter: counterReducer,
// Nếu có các slice khác, bạn thêm vào đây tương tự:
// users: usersReducer,
// products: productsReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: { counter: CounterState, users: UsersState, ... }
// Infer the `AppDispatch` type from the store's dispatch function
export type AppDispatch = typeof store.dispatch;
// Inferred type: ThunkDispatch<RootState, undefined, AnyAction> & Dispatch<AnyAction>
Giải thích:
- Chúng ta nhập
counterReducer
từ filecounterSlice.ts
. - Trong object
reducer
củaconfigureStore
, chúng ta thêm một cặp key-value:counter: counterReducer
. Keycounter
xác định tên của phần trạng thái này trong store tổng thể. Bây giờ, trạng thái của slice counter sẽ được truy cập thông quastate.counter
. - Điều kỳ diệu của TypeScript: Khi bạn thêm
counter: counterReducer
vào objectreducer
, TypeScript sẽ tự động cập nhật kiểuRootState
. Bây giờ,RootState
sẽ biết rằng nó có một trườngcounter
với kiểu dữ liệu làCounterState
(đã định nghĩa trongcounterSlice.ts
). Điều này hoàn toàn tự động nhờReturnType<typeof store.getState>
.
Bước 7: Sử dụng State và Dispatch Actions trong Component React
Cuối cùng, chúng ta sẽ sử dụng các typed hooks (useAppSelector
, useAppDispatch
) và các action creator từ slice trong một component React.
Tạo một component mới, ví dụ src/features/counter/Counter.tsx
.
// src/features/counter/Counter.tsx
import React from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks'; // Nhập hooks đã được typed
import { increment, decrement, incrementByAmount } from './counterSlice'; // Nhập các action creator từ slice
function Counter() {
// Sử dụng useAppSelector để đọc giá trị 'value' từ trạng thái slice 'counter'
// TypeScript biết rằng state.counter.value là một số (number)
const count = useAppSelector(state => state.counter.value);
// Nếu bạn có các trường khác trong CounterState, bạn có thể truy cập chúng tương tự:
// const status = useAppSelector(state => state.counter.status);
// Sử dụng useAppDispatch để lấy hàm dispatch đã được typed
const dispatch = useAppDispatch();
return (
<div>
<h2>Giá trị Bộ đếm: {count}</h2>
<div>
<button onClick={() => dispatch(increment())}>Tăng</button>
<button onClick={() => dispatch(decrement())}>Giảm</button>
{/* Dispatch action với payload */}
<button onClick={() => dispatch(incrementByAmount(5))}>Tăng 5</button>
</div>
{/* Hiển thị trạng thái khác nếu có */}
{/* <p>Trạng thái: {status}</p> */}
</div>
);
}
export default Counter;
Giải thích:
- Chúng ta nhập
useAppSelector
vàuseAppDispatch
từ filehooks.ts
. - Chúng ta nhập các action creator
increment
,decrement
,incrementByAmount
từ filecounterSlice.ts
. const count = useAppSelector(state => state.counter.value);
:useAppSelector
được gọi với một selector function.- Selector function nhận vào toàn bộ trạng thái gốc (
state
). - Nhờ
TypedUseSelectorHook<RootState>
, TypeScript biết cấu trúc củastate
. Nó biết rằngstate
có trườngcounter
, vàstate.counter
có trườngvalue
. Kiểu củastate.counter.value
được suy ra lànumber
, giống như bạn đã định nghĩa trongCounterState
. Bạn sẽ nhận được gợi ý code và kiểm tra lỗi kiểu ngay lập tức!
const dispatch = useAppDispatch();
: Lấy hàmdispatch
đã được typed.onClick={() => dispatch(increment())}
: Khi button được click, chúng ta gọidispatch
và truyền vào kết quả của việc gọi action creatorincrement()
.increment()
trả về một action object ({ type: 'counter/increment' }
). TypeScript biết rằngdispatch
chấp nhận action object này.onClick={() => dispatch(incrementByAmount(5))}
: Khi button "Tăng 5" được click, chúng ta gọiincrementByAmount(5)
. Action creator này nhận một đối số (payload) và trả về action object ({ type: 'counter/incrementByAmount', payload: 5 }
). TypeScript biết rằngdispatch
chấp nhận action này vàpayload
của nó phải là một số.
Cuối cùng, bạn có thể thêm component Counter
này vào App.tsx
để hiển thị nó:
// src/App.tsx
import React from 'react';
import './App.css';
import Counter from './features/counter/Counter'; // Nhập component Counter
function App() {
return (
<div className="App">
<header className="App-header">
{/* Sử dụng component Counter đã kết nối với Redux */}
<Counter />
<p>
Ví dụ về tích hợp Redux với TypeScript
</p>
</header>
</div>
);
}
export default App;
Comments