Skip to content
Home
Go back

Redux: Store, Action, Reducer và Redux Toolkit

Mục lục

Xem mục lục

Redux giải quyết vấn đề gì

Redux quản lý state dùng chung giữa nhiều component không liên quan trực tiếp trong cây component, tránh phải truyền state qua nhiều tầng props (props drilling) chỉ để tới được component cần dùng.

Context API cũng giải quyết được vấn đề tương tự ở mức cơ bản, nhưng Redux phù hợp hơn khi:

Ba nguyên tắc cốt lõi

Redux xây dựng trên 3 nguyên tắc:

Store, Action, Reducer là gì

Ví dụ tối thiểu bằng Redux thuần (không dùng Toolkit) để thấy rõ cơ chế gốc:

import { createStore } from "redux";

// Reducer: pure function, nhận state + action, trả về state mới
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

// Store: nơi giữ state, tạo ra từ reducer
const store = createStore(counterReducer);

// Action: object mô tả điều gì đã xảy ra, luôn có field `type`
store.dispatch({ type: "INCREMENT" });

console.log(store.getState()); // { count: 1 }

dispatch gửi action tới store, store gọi reducer với state hiện tại và action đó, nhận về state mới, rồi thông báo cho mọi nơi đang lắng nghe (subscribe) biết state đã đổi.

Vấn đề của Redux thuần: boilerplate

Một thao tác đơn giản như “tăng số đếm” trong Redux thuần thường cần tới 3-4 phần riêng biệt: action type constant, action creator, nhánh switch-case trong reducer, và cập nhật state theo kiểu immutable thủ công:

// action-types.js
export const INCREMENT = "counter/increment";

// actions.js
export function increment() {
  return { type: INCREMENT };
}

// reducer.js
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 }; // phải tự spread, tự viết immutable
    default:
      return state;
  }
}

Với state lồng nhiều cấp (object trong object, mảng trong object), việc tự viết immutable update (...state, ...state.nested) càng dài dòng và dễ sai sót, đặc biệt khi cần cập nhật một field sâu bên trong cấu trúc phức tạp.

Redux Toolkit: cách viết hiện đại

Redux Toolkit (RTK) là bộ công cụ chính thức, gộp action creator và reducer vào chung một chỗ qua createSlice, đồng thời dùng thư viện Immer bên dưới để cho phép viết code “trông như mutate” nhưng thực chất Immer tự tạo ra state mới immutable:

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { count: 0 },
  reducers: {
    increment: state => {
      state.count += 1; // trông như mutate, nhưng Immer xử lý thành immutable update
    },
    decrement: state => {
      state.count -= 1;
    },
    incrementBy: (state, action) => {
      state.count += action.payload;
    },
  },
});

export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;

createSlice tự sinh action creator (increment(), decrement(), incrementBy(5)) và action type ("counter/increment") từ tên field trong reducers, không cần viết tay như Redux thuần. So với ví dụ trước, cùng một logic nhưng ngắn hơn đáng kể, không cần action type constant riêng, không cần tự viết ...state.

dispatch và kết nối component

Thư viện react-redux cung cấp useSelector (đọc state) và useDispatch (gửi action):

import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";

function Counter() {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(decrement())}>-1</button>
    </div>
  );
}

Luồng dữ liệu một chiều: component gọi dispatch(action) → store chạy reducer → state mới được lưu vào store → mọi component đang useSelector đọc đúng phần state vừa đổi tự động re-render với giá trị mới. Component không bao giờ sửa trực tiếp state, luôn phải đi qua action.

Async logic: createAsyncThunk

Reducer bắt buộc phải là pure function, không được gọi API hay gây side effect trực tiếp bên trong. createAsyncThunk tách phần gọi API ra riêng, tự động sinh ra 3 action tương ứng với 3 trạng thái của một lời gọi bất đồng bộ:

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

export const fetchUser = createAsyncThunk("user/fetch", async (userId) => {
  const res = await api.getUser(userId);
  return res.data;
});

const userSlice = createSlice({
  name: "user",
  initialState: { data: null, status: "idle" },
  reducers: {},
  extraReducers: builder => {
    builder
      .addCase(fetchUser.pending, state => {
        state.status = "loading";
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.data = action.payload;
      })
      .addCase(fetchUser.rejected, state => {
        state.status = "failed";
      });
  },
});

Gọi dispatch(fetchUser(userId)) sẽ tự động dispatch lần lượt pending (khi bắt đầu gọi), rồi fulfilled (nếu thành công) hoặc rejected (nếu lỗi), không cần tự quản lý 3 trạng thái này bằng tay như viết useState rời rạc.

So sánh Redux với Context API và React Query

Ba công cụ giải quyết ba vấn đề khác nhau, không thay thế hoàn toàn cho nhau:

Công cụPhù hợp choKhông phù hợp cho
Context APIState đơn giản, ít thay đổi, phạm vi nhỏ (theme, ngôn ngữ, thông tin user đăng nhập)State phức tạp, thay đổi thường xuyên (dễ gây re-render toàn bộ consumer, đã nói ở bài Tối ưu hiệu năng React)
ReduxGlobal client state phức tạp, nhiều action, cần middleware/devtools mạnh, logic nghiệp vụ phức tạpCache dữ liệu từ server (tự quản lý loading/error/refetch/stale time thủ công, tốn công hơn hẳn)
React QueryServer state: dữ liệu từ API, cần cache, refetch, đồng bộ nhiều nơi (đã nói ở bài Optimistic Updates)Client state thuần túy không liên quan tới server (trạng thái UI, form đang mở/đóng)

Nhầm lẫn phổ biến: dùng Redux để lưu cache dữ liệu server, tự viết lại toàn bộ cơ chế loading/error/refetch/cache invalidation mà React Query đã làm sẵn. Redux phù hợp nhất với state thuộc về client, không phải bản sao của dữ liệu server.

Xu hướng hiện nay

Từ khi React Query (và các thư viện tương tự như SWR) phổ biến, phần lớn lý do trước đây phải dùng Redux (đặc biệt là createAsyncThunk/redux-thunk chỉ để fetch data) không còn cần thiết nữa. createAsyncThunk sinh ra chủ yếu để chuẩn hóa việc gọi API và quản lý pending/fulfilled/rejected, nhưng React Query làm đúng việc đó tốt hơn, ít code hơn, có sẵn cache/refetch/stale time.

Xu hướng phổ biến ở các dự án mới:

Nói ngắn gọn: Redux không “chết”, nhưng phạm vi sử dụng đã thu hẹp lại đáng kể, từ chỗ gần như mặc định cho mọi ứng dụng React cỡ vừa trở lên, xuống thành lựa chọn cho một nhóm bài toán cụ thể hơn nhiều.

Tổng kết


Share this post:

Previous Post
Optimistic Updates với React Query
Next Post
P1: Testing pyramid: Unit, Integration, E2E khác nhau ở đâu