Kiến trúc Redux và các mẹo thực tiễn với Redux Toolkit

Vấn đề Redux giải quyết

Khi ứng dụng React phình to, việc truyền state qua nhiều cấp component trở nên cồng kềnh. Nhiều component ở các nhánh khác nhau cùng cần một dữ liệu, nhưng state chỉ có thể đi từ trên xuống qua props:

App
├── Header
├── Sidebar
└── Main
    └── Dashboard

Nếu cả Header, Sidebar và Dashboard đều cần thông tin tài khoản, ta phải đưa state lên tầng App và truyền xuống từng tầng. Đây chính là "prop drilling". Redux giải quyết bằng cách tập trung state vào một Store chung, component nào cũng truy cập được trực tiếp.

Luồng hoạt động cơ bản

Chuỗi xử lý trong Redux gồm 4 bước:

View phát ra Action
    ↓
Reducer xử lý Action
    ↓
Store cập nhật state
    ↓
View render lại
Thành phầnVai trò
ActionObject mô tả sự kiện vừa xảy ra
ReducerHàm thuần trả về state mới dựa trên action
StoreNơi lưu duy nhất toàn bộ state toàn cục

Viết mã hiện đại với Redux Toolkit

Thay vì định nghĩa action type, action creator và reducer riêng lẻ, Redux Toolkit gói chúng lại trong createSlice:

import { createSlice } from '@reduxjs/toolkit'

const scoreSlice = createSlice({
  name: 'score',
  initialState: { points: 0 },
  reducers: {
    increase: state => { state.points += 1 },
    decrease: state => { state.points -= 1 },
    addBonus: (state, action) => { state.points += action.payload },
  }
})

export const { increase, decrease, addBonus } = scoreSlice.actions
export default scoreSlice.reducer

Lợi ích chính:

  • Tự động tạo action type và action creator.
  • Tích hợp Immer, cho phép viết mutate trực tiếp nhưng vẫn đảm bảo immutability.
  • Cắt giảm đáng kể boilerplate so với cách viết truyền thống.

Kết nối với React

import { useSelector, useDispatch } from 'react-redux'
import { increase, addBonus } from './scoreSlice'

function ScoreBoard() {
  const points = useSelector(state => state.score.points)
  const dispatch = useDispatch()

  return (
    <div>
      <p>{points}</p>
      <button onClick={() => dispatch(increase())}>+1</button>
      <button onClick={() => dispatch(addBonus(10))}>+10</button>
    </div>
  )
}

Xử lý tác vụ bất đng bộ

createAsyncThunk giúp quản lý các thao tác API mà không cần tự viết action cho từng trạng thái:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

export const loadProfile = createAsyncThunk(
  'profile/load',
  async (profileId) => {
    const res = await fetch(`/api/profiles/${profileId}`)
    return res.json()
  }
)

const profileSlice = createSlice({
  name: 'profile',
  initialState: { info: null, isLoading: false, error: null },
  reducers: {},
  extraReducers: builder => {
    builder
      .addCase(loadProfile.pending, state => {
        state.isLoading = true
      })
      .addCase(loadProfile.fulfilled, (state, action) => {
        state.isLoading = false
        state.info = action.payload
      })
      .addCase(loadProfile.rejected, (state, action) => {
        state.isLoading = false
        state.error = action.error.message
      })
  }
})

Mẹo 1: Tách selector ra khỏi component

// Không nên: logic nặng trong component
const total = useSelector(state => 
  state.basket.goods.reduce((sum, i) => sum + i.cost * i.amount, 0)
)

// Nên: đưa vào slice, tái sử dụng
export const selectBasketTotal = state =>
  state.basket.goods.reduce((sum, i) => sum + i.cost * i.amount, 0)

// Trong component
const total = useSelector(selectBasketTotal)

Mẹo 2: Memo hóa với createSelector

import { createSelector } from '@reduxjs/toolkit'

const selectGoods = state => state.basket.goods

export const selectBasketTotal = createSelector(
  [selectGoods],
  goods => goods.reduce((sum, i) => sum + i.cost * i.amount, 0)
)

Mẹo 3: Chia slice theo chức năng

// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import accountReducer from './accountSlice'
import basketReducer from './basketSlice'

export const store = configureStore({
  reducer: {
    account: accountReducer,
    basket: basketReducer,
  }
})

Mẹo 4: Ch đưa state toàn cục thực sự vào Redux

Loại stateNơi lưu
Dùng chung nhiều nơiRedux
Chỉ component đó dùnguseState
Dữ liệu t serverSWR / TanStack Query
FormReact Hook Form
State trên URLURL params

Khi nào không cần Redux

  • Dự án nhỏ: useState + useContext đủ dùng.
  • Dữ liệu server: dùng thư viện chuyên trị như TanStack Query.
  • Form phức tạp: dùng React Hook Form.

Redux phát huy khi ứng dụng lớn cần state chia sẻ rõ ràng giữa nhiều nhánh component, ví dụ thông tin đăng nhập, quyền truy cập, giỏ hàng, giao diện chủ đề.

Thẻ: redux Redux Toolkit react createAsyncThunk createSelector

Đăng vào ngày 10 tháng 10 lúc 01:12