Bạn là một lập trình viên React? Bạn đã từng nghe đến Redux và cảm thấy hoang mang? Hay bạn đang xây dựng một ứng dụng React và bắt đầu "phát điên" vì phải truyền props
qua hàng chục component khác nhau?
Nếu câu trả lời là "Có", thì bạn đã đến đúng nơi.
Bài viết này sẽ giúp bạn giải mã mọi thứ về Redux và sự kết hợp của nó với ReactJS. Chúng ta sẽ cùng nhau đi từ những khái niệm cơ bản nhất, khám phá lý do tại sao Redux ra đời, cách nó hoạt động, và quan trọng nhất, khi nào bạn thực sự cần đến nó.
Hãy bắt đầu hành trình này!
1. Cơn ác mộng mang tên "Prop Drilling" và sự ra đời của Redux
Hãy tưởng tượng bạn đang xây dựng một ứng dụng lớn, ví dụ như một trang thương mại điện tử. Bạn có thông tin người dùng (họ tên, ảnh đại diện, giỏ hàng) ở component gốc App
. Bây giờ, bạn muốn hiển thị tên người dùng ở Header
và số lượng sản phẩm trong giỏ hàng ở một component MiniCart
nằm sâu bên trong Header
.
Theo cách thông thường của React, bạn sẽ phải làm gì?
- Truyền
userInfo
từApp
xuốngHomePage
. - Truyền
userInfo
từHomePage
xuốngHeader
. Header
sử dụnguserInfo.name
và lại tiếp tục truyềnuserInfo.cart
xuống componentMiniCart
.
Cái chuỗi truyền dữ liệu dài dằng dặc qua nhiều tầng component trung gian không hề cần đến nó được gọi là "Prop Drilling".
Đây chính là vấn đề lớn mà Redux được sinh ra để giải quyết.
Redux là gì? Một cách định nghĩa dễ hiểu nhất
Redux là một thư viện quản lý trạng thái (state management) có thể dự đoán được cho các ứng dụng JavaScript. Nó không phải là một phần của React, mà là một thư viện độc lập có thể dùng với React, Angular, Vue hay thậm chí là Vanilla JS.
Hãy hình dung Redux như một "kho chứa đồ chung" cho toàn bộ ứng dụng của bạn. Thay vì mỗi component tự giữ trạng thái riêng và truyền cho nhau một cách lộn xộn, tất cả trạng thái quan trọng (như thông tin người dùng, cài đặt theme, dữ liệu sản phẩm...) sẽ được lưu trữ tại một nơi duy nhất gọi là Store.
Bất kỳ component nào, dù ở sâu đến đâu trong cây component, đều có thể "kết nối" trực tiếp với "kho" này để lấy dữ liệu nó cần hoặc yêu cầu thay đổi dữ liệu mà không cần phải đi qua các component trung gian.
Phần 2: Ba khái niệm "core" của Redux
Để hiểu Redux, bạn cần nắm vững 3 khái niệm cốt lõi. Chúng tạo thành một luồng dữ liệu một chiều (unidirectional data flow) rất rõ ràng và dễ gỡ lỗi.
1. Store
Đây là trái tim của Redux. Store là một đối tượng JavaScript khổng lồ chứa toàn bộ trạng thái (state) của ứng dụng.
- Duy nhất: Toàn bộ ứng dụng của bạn chỉ có duy nhất MỘT Store. Đây được gọi là "Single Source of Truth".
- Chỉ đọc (Read-only): Bạn không bao giờ được phép thay đổi trực tiếp state trong Store. Ví dụ:
store.state.user = 'new user'
là CẤM. - Cách thay đổi: Cách duy nhất để thay đổi state là gửi đi một "Action".
2. Actions
Nếu bạn muốn thay đổi trạng thái trong Store, bạn phải "gửi yêu cầu". Action chính là bản yêu cầu đó.
- Là một object JavaScript đơn thuần: Một Action phải có thuộc tính
type
để mô tả hành động, ví dụ:'auth/loginSuccess'
,'cart/addProduct'
. - Có thể chứa dữ liệu kèm theo (payload): Ngoài
type
, Action có thể mang theo dữ liệu cần thiết cho việc thay đổi state. Dữ liệu này thường được đặt trong thuộc tínhpayload
.
Ví dụ một Action:
// Action để thêm một sản phẩm vào giỏ hàng
{
type: 'cart/addProduct',
payload: {
productId: 'prod_123',
name: 'Laptop ABC',
price: 20000000
}
}
3. Reducers
Khi một Action được gửi đi (dispatch), Redux sẽ chuyển nó đến cho Reducer.
- Là một hàm thuần túy (Pure Function): Reducer nhận vào 2 tham số:
(currentState, action)
và trả về mộtnewState
. - Không bao giờ thay đổi state cũ: Thay vì sửa state hiện tại, Reducer phải tạo ra một bản sao của state, thay đổi trên bản sao đó và trả về bản sao mới. Đây là nguyên tắc bất biến (immutability).
- Xử lý dựa trên
action.type
: Bên trong Reducer, bạn sẽ dùngswitch...case
hoặcif...else
để kiểm traaction.type
và quyết định cách cập nhật state.
Ví dụ một Reducer đơn giản:
const initialState = {
items: [],
totalAmount: 0,
}
function cartReducer(state = initialState, action) {
switch (action.type) {
case 'cart/addProduct':
// Tạo một bản sao mới của state
const newState = {
...state,
// Thêm sản phẩm mới vào mảng items
items: [...state.items, action.payload],
// Cập nhật tổng tiền
totalAmount: state.totalAmount + action.payload.price,
}
// Trả về state mới
return newState
default:
// Nếu action không khớp, trả về state hiện tại
return state
}
}
Luồng hoạt động đầy đủ:
- UI Event: Người dùng click vào nút "Thêm vào giỏ hàng".
- Dispatch Action: Component gọi hàm
dispatch()
để gửi đi một Action{ type: 'cart/addProduct', payload: {...} }
. - Reducer xử lý: Store nhận Action và chuyển cho
cartReducer
. Reducer này xử lý và trả về một state mới của giỏ hàng. - Store cập nhật: Store thay thế state cũ bằng state mới mà Reducer vừa trả về.
- UI cập nhật: Các component React nào đang "lắng nghe" sự thay đổi của giỏ hàng sẽ tự động được render lại với dữ liệu mới.
Phần 3: React Redux - Cầu nối hoàn hảo
Như đã nói, Redux là một thư viện độc lập. Để kết nối nó với React một cách hiệu quả, chúng ta sử dụng thư viện react-redux
. Thư viện này cung cấp các hook và component giúp việc tương tác trở nên dễ dàng.
<Provider store={store}>
: Một component dùng để bọc toàn bộ ứng dụng của bạn (<App />
). Nó giúp mọi component con có thể truy cập vào Redux store.useSelector()
: Một hook cho phép component "đọc" hoặc "lấy" dữ liệu từ store. Nó nhận vào một hàm để chọn ra mảnh state mà component cần.useDispatch()
: Một hook trả về hàmdispatch
của store. Bạn dùng hàm này để gửi đi các Actions.
Phần 4: Redux Toolkit (RTK) - Phiên bản nâng cấp
Nhiều lập trình viên cho rằng Redux "cổ điển" khá dài dòng và phải viết nhiều mã lặp đi lặp lại (boilerplate). Nhận biết điều này, đội ngũ Redux đã tạo ra Redux Toolkit (RTK).
Redux Toolkit là bộ công cụ chính thức, được đề xuất để phát triển ứng dụng Redux. Nó giúp đơn giản hóa và tăng tốc quá trình code Redux.
RTK giải quyết các vấn đề của Redux cũ bằng cách:
- Giảm code boilerplate: Với hàm
createSlice
, bạn có thể định nghĩa Reducer và Actions trong cùng một file một cách ngắn gọn. - Cấu hình Store đơn giản:
configureStore
tự động thiết lập những thứ cần thiết, bao gồm cả Redux DevTools. - Tích hợp sẵn ImmerJS: Giúp bạn viết code thay đổi state trông như thể bạn đang sửa trực tiếp (mutable), nhưng thực chất nó vẫn đảm bảo tính bất biến (immutable) ở phía sau.
Nếu bạn bắt đầu với Redux hôm nay, hãy sử dụng Redux Toolkit.
// Tạo Slice với createSlice
import { createSlice } from '@reduxjs/toolkit'
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
totalAmount: 0,
isLoading: false,
},
reducers: {
addProduct: (state, action) => {
// Với Immer, bạn có thể "sửa" state trực tiếp
state.items.push(action.payload)
state.totalAmount += action.payload.price
},
removeProduct: (state, action) => {
const index = state.items.findIndex(
(item) => item.id === action.payload.id,
)
if (index !== -1) {
const removedItem = state.items[index]
state.items.splice(index, 1)
state.totalAmount -= removedItem.price
}
},
clearCart: (state) => {
state.items = []
state.totalAmount = 0
},
setLoading: (state, action) => {
state.isLoading = action.payload
},
},
})
// Tự động tạo ra actions
export const { addProduct, removeProduct, clearCart, setLoading } =
cartSlice.actions
// Tự động tạo ra reducer
export default cartSlice.reducer
Phần 5: Ưu, nhược điểm và khi nào nên dùng Redux?
Redux rất mạnh mẽ, nhưng không phải là "viên đạn bạc" cho mọi dự án.
Ưu điểm:
- Dự đoán được (Predictable): Luồng dữ liệu một chiều giúp bạn dễ dàng biết được state thay đổi ở đâu, khi nào và tại sao.
- Quản lý tập trung: Mọi thứ ở một nơi, dễ dàng quản lý và mở rộng.
- Gỡ lỗi (Debugging) tuyệt vời: Với Redux DevTools, bạn có thể "time travel", xem lại từng action và sự thay đổi của state.
- Hệ sinh thái lớn: Rất nhiều thư viện hỗ trợ (redux-persist, redux-saga, redux-thunk...).
Nhược điểm:
- Đường cong học tập (Learning Curve): Cần thời gian để hiểu rõ các khái niệm.
- Dài dòng (Verbosity): Ngay cả với RTK, việc thiết lập cho các tác vụ đơn giản vẫn có thể tốn nhiều công sức hơn so với state nội bộ của component.
- Không dành cho tất cả: Dùng Redux cho một ứng dụng nhỏ, đơn giản là "dùng dao mổ trâu để giết gà".
Vậy khi nào nên dùng Redux?
- Ứng dụng lớn và phức tạp: Khi state được chia sẻ bởi nhiều component không liên quan trực tiếp.
- Trạng thái ứng dụng thay đổi thường xuyên: Cần một cơ chế quản lý chặt chẽ.
- Cần các tính năng gỡ lỗi nâng cao: Như time-travel debugging.
- Làm việc trong đội nhóm lớn: Cấu trúc rõ ràng của Redux giúp các thành viên dễ dàng phối hợp.
Các giải pháp thay thế Redux
- React Context API: Tích hợp sẵn trong React, tốt cho việc truyền dữ liệu đơn giản, tránh prop drilling ở quy mô nhỏ.
- Zustand, Jotai: Các thư viện quản lý state hiện đại, tối giản và ít boilerplate hơn Redux.
- MobX: Một giải pháp mạnh mẽ khác dựa trên lập trình phản ứng (Reactive Programming).
Redux không phải là một khái niệm đáng sợ. Nó là một công cụ mạnh mẽ, một giải pháp kiến trúc tuyệt vời cho bài toán quản lý trạng thái trong các ứng dụng JavaScript quy mô lớn. Bằng cách tập trung toàn bộ state vào một "kho" duy nhất và áp đặt một luồng dữ liệu một chiều, Redux mang lại sự rõ ràng, dễ dự đoán và khả năng gỡ lỗi phi thường.
Với sự ra đời của Redux Toolkit, việc sử dụng Redux đã trở nên đơn giản và hiệu quả hơn bao giờ hết.
Hy vọng rằng qua bài viết này, bạn không chỉ hiểu "Redux trong React là gì" mà còn có thể tự tin quyết định xem nó có phù hợp với dự án tiếp theo của mình hay không.
Chúc bạn thành công trên con đường chinh phục React và Redux!