Bài 21.5: Bài tập thực hành Redux

Chào mừng trở lại series blog về Lập trình Web Front-end! Sau khi đã cùng nhau đi qua lý thuyết và các khái niệm cốt lõi của Redux, giờ là lúc chúng ta đưa kiến thức đó vào thực tế. Không có gì hiệu quả hơn việc tự tay code để thực sự hiểu cách một thư viện hay framework hoạt động.

Trong bài thực hành này, chúng ta sẽ xây dựng một ứng dụng cực kỳ phổ biến trong thế giới Redux: một Bộ đếm số đơn giản (Counter). Nghe có vẻ đơn giản, nhưng nó sẽ bao gồm tất cả các thành phần cốt lõi của Redux và cách tích hợp chúng với React.

Hãy chuẩn bị môi trường code của bạn và cùng bắt đầu nào!

Chuẩn Bị

Đảm bảo bạn đã có một dự án React cơ bản. Nếu chưa, bạn có thể tạo nhanh bằng Create React App (dù không còn được khuyến khích cho dự án mới, nhưng đủ dùng cho bài tập này) hoặc Vite:

# Sử dụng Vite
npm create vite my-redux-app --template react
cd my-redux-app
npm install

# Cài đặt các thư viện cần thiết
npm install redux react-redux @reduxjs/toolkit

Giải thích:

  • redux: Thư viện lõi của Redux.
  • react-redux: Cung cấp các liên kết (bindings) để Redux hoạt động dễ dàng với React (như Provider, useSelector, useDispatch).
  • @reduxjs/toolkit: Bộ công cụ được khuyến nghị từ Redux để giúp viết code Redux đơn giản và hiệu quả hơn (giảm boilerplate). Chúng ta sẽ sử dụng nó để thiết lập store.
Cấu Trúc Dự Án (Gợi ý)

Để giữ cho mọi thứ có tổ chức, bạn có thể tạo cấu trúc thư mục đơn giản như sau trong thư mục src:

src/
├── actions/
│   └── counterActions.js
├── reducers/
│   └── counterReducer.js
├── components/
│   └── Counter.js
├── store.js
└── index.js (hoặc App.js)
Bước 1: Định Nghĩa Actions

Actions là các object đơn giản mô tả điều gì đã xảy ra trong ứng dụng. Chúng có thuộc tính type bắt buộc.

Chúng ta sẽ định nghĩa các action để tăng, giảm và đặt lại bộ đếm.

// src/actions/counterActions.js

// Định nghĩa các loại action dưới dạng hằng số để tránh lỗi chính tả
export const INCREMENT = 'counter/increment'; // Quy ước đặt tên slice/action
export const DECREMENT = 'counter/decrement';
export const RESET = 'counter/reset';

// Action Creators: Các hàm tạo ra action object
export const increment = () => ({
  type: INCREMENT
});

export const decrement = () => ({
  type: DECREMENT
});

export const reset = () => ({
  type: RESET
});

Giải thích:

  • Chúng ta sử dụng hằng số để định nghĩa type của action. Điều này giúp trình soạn thảo code (IDE) bắt lỗi chính tả và dễ dàng gỡ lỗi hơn.
  • action creators là các hàm trả về action object. Việc sử dụng chúng giúp code của bạn dễ đọc và tái sử dụng hơn khi cần dispatch action.
Bước 2: Xây Dựng Reducer

Reducer là hàm thuần khiết (pure function) nhận vào trạng thái hiện tại (state) và action, sau đó trả về trạng thái mới. Reducer là nơi duy nhất logic thay đổi trạng thái được xử lý.

// src/reducers/counterReducer.js

import { INCREMENT, DECREMENT, RESET } from '../actions/counterActions';

// Định nghĩa trạng thái ban đầu
const initialState = {
  count: 0
};

// Reducer cho bộ đếm
const counterReducer = (state = initialState, action) => {
  // Sử dụng switch statement để xử lý các loại action khác nhau
  switch (action.type) {
    case INCREMENT:
      // Trả về trạng thái mới bằng cách sao chép trạng thái cũ
      // và cập nhật thuộc tính 'count'
      return {
        ...state, // Sao chép tất cả các thuộc tính khác của state (nếu có)
        count: state.count + 1
      };
    case DECREMENT:
       // Đảm bảo count không âm (tùy chọn logic)
      return {
        ...state,
        count: state.count > 0 ? state.count - 1 : 0
      };
    case RESET:
      return {
        ...state,
        count: 0
      };
    default:
      // Nếu action không khớp với bất kỳ case nào,
      // luôn trả về trạng thái hiện tại mà không thay đổi
      return state;
  }
};

export default counterReducer;

Giải thích:

  • Reducer nhận state (với giá trị mặc định là initialState khi khởi tạo) và action.
  • Điều quan trọng nhất là reducer không được thay đổi trực tiếp trạng thái hiện tại (state). Thay vào đó, nó phải trả về một object trạng thái mới. Chúng ta dùng cú pháp spread (...state) để sao chép trạng thái cũ và chỉ cập nhật những phần cần thay đổi.
  • default case là bắt buộc để đảm bảo reducer luôn trả về trạng thái hiện tại nếu nhận được một action mà nó không xử lý.
Bước 3: Thiết Lập Store

Store là nơi duy nhất chứa toàn bộ trạng thái của ứng dụng. Chúng ta sẽ sử dụng configureStore từ Redux Toolkit để thiết lập nó.

// src/store.js

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './reducers/counterReducer';

// Cấu hình store
const store = configureStore({
  reducer: {
    // Đăng ký reducer của bạn ở đây.
    // Key 'counter' sẽ là tên của phần slice state này trong store gốc.
    counter: counterReducer
    // Nếu có nhiều reducers khác, bạn thêm vào đây:
    // user: userReducer,
    // products: productsReducer
  }
});

export default store;

Giải thích:

  • configureStore là hàm được đề xuất bởi Redux Toolkit để tạo store. Nó tự động kết hợp reducers, thiết lập Redux DevTools Extension và thêm một số middleware hữu ích theo mặc định.
  • Chúng ta truyền vào một object reducer, nơi mỗi key (ví dụ: 'counter') tương ứng với một slice (lát cắt) của trạng thái gốc trong store, và value là reducer quản lý slice đó.
Bước 4: Kết Nối Redux với React App

Để các component React có thể truy cập store, chúng ta cần bọc toàn bộ ứng dụng bằng component Provider từ react-redux.

Mở file gốc của ứng dụng (thường là src/index.js hoặc src/main.jsx nếu dùng Vite):

// src/index.js (hoặc tương tự)

import React from 'react';
import ReactDOM from 'react-dom/client'; // hoặc 'react-dom'
import { Provider } from 'react-redux'; // Import Provider
import store from './store'; // Import store
import App from './App'; // Component gốc của ứng dụng

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode> {/* React.StrictMode giúp phát hiện vấn đề tiềm ẩn */}
    <Provider store={store}> {/* Bọc App bằng Provider và truyền store */}
      <App />
    </Provider>
  </React.StrictMode>
);

Giải thích:

  • Provider là một component cấp cao (Higher-Order Component) từ react-redux. Nó sử dụng React Context để đưa store Redux vào cây component, giúp mọi component con bên trong có thể truy cập store mà không cần prop drilling.
  • Bạn cần truyền instance store đã tạo ở Bước 3 vào prop store của Provider.
Bước 5: Sử Dụng State và Dispatch Actions trong Component React

Cuối cùng, chúng ta sẽ tạo một component React (Counter.js) để hiển thị giá trị của bộ đếm và các nút để tương tác với nó. Chúng ta sẽ sử dụng các React Hooks được cung cấp bởi react-redux: useSelector để đọc state và useDispatch để gửi actions.

// src/components/Counter.js

import React from 'react';
// Import các hooks từ react-redux
import { useSelector, useDispatch } from 'react-redux';
// Import các action creators đã định nghĩa
import { increment, decrement, reset } from '../actions/counterActions';

function Counter() {
  // Sử dụng useSelector để đọc giá trị 'count' từ slice 'counter' trong state Redux
  const count = useSelector(state => state.counter.count);

  // Sử dụng useDispatch để lấy hàm dispatch
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Bộ đếm Redux</h1>
      <p>Giá trị hiện tại: <strong>{count}</strong></p> {/* Hiển thị giá trị */}
      <button onClick={() => dispatch(increment())}>Tăng (+)</button> {/* Nút Tăng */}
      <button onClick={() => dispatch(decrement())}>Giảm (-)</button> {/* Nút Giảm */}
      <button onClick={() => dispatch(reset())}>Đặt lại</button> {/* Nút Đặt lại */}
    </div>
  );
}

export default Counter;

Giải thích:

  • useSelector(selectorFunction): Hook này cho phép bạn "chọn" (select) một phần dữ liệu cụ thể từ trạng thái gốc của Redux store. Hàm selectorFunction nhận toàn bộ trạng thái gốc (state) và trả về dữ liệu bạn muốn component sử dụng. Ở đây, chúng ta lấy state.counter.count. Component sẽ tự động re-render khi giá trị mà selector trả về thay đổi.
  • useDispatch(): Hook này trả về hàm dispatch từ Redux store. Bạn sử dụng hàm dispatch này để gửi (fire) một action đến store.
  • Trong các event handler (onClick), chúng ta gọi dispatch() và truyền vào kết quả của việc gọi các action creators (ví dụ: increment()). Khi dispatch(increment()) được gọi, action { type: 'counter/increment' } sẽ được gửi đến reducer, reducer xử lý và cập nhật state, useSelector nhận thấy state đã thay đổi và component Counter re-render với giá trị count mới.
Bước 6: Hiển Thị Component Counter

Cuối cùng, hãy thêm component Counter vào component gốc của ứng dụng (App.js):

// src/App.js

import React from 'react';
import Counter from './components/Counter'; // Import component Counter

function App() {
  return (
    <div className="App" style={{ textAlign: 'center', marginTop: '50px' }}>
      {/* Các nội dung khác của App */}
      <Counter /> {/* Hiển thị component Counter */}
      {/* Các nội dung khác */}
    </div>
  );
}

export default App;
Chạy Ứng Dụng

Bây giờ, bạn có thể chạy ứng dụng React của mình:

npm start
# hoặc nếu dùng Vite
npm run dev

Mở trình duyệt tại địa chỉ tương ứng (thường là http://localhost:3000 hoặc http://localhost:5173), bạn sẽ thấy giao diện bộ đếm đơn giản hoạt động. Khi bạn click vào các nút "Tăng" hoặc "Giảm", giá trị hiển thị sẽ thay đổi, và sự thay đổi này được quản lý hoàn toàn bởi Redux.

Comments

There are no comments at the moment.