Quản Lý Trạng Thái Trong Ứng Dụng React Với Redux

Trong các ứng dụng React, khi quy mô dự án mở rộng, việc quản lý trạng thái tập trung trở nên cực kỳ quan trọng. Mặc dù Vuex là một thư viện quản lý trạng thái mạnh mẽ trong hệ sinh thái Vue.js, nhưng nó không thể được sử dụng trực tiếp trong React. Redux, với kiến trúc tương tự và độc lập với framework, có thể được tích hợp liền mạch vào React, giúp các nhà phát triển quản lý các thay đổi trạng thái phức tạp. Bài viết này sẽ giới thiệu các khái niệm cốt lõi của Redux như Store, Reducers, Actions, Middleware và hàm connect, đồng thời hướng dẫn cách triển khai quản lý trạng thái trong dự án React.

1. Tầm Quan Trọng Của Quản Lý Trạng Thái Trong Ứng Dụng React

Khi xây dựng các ứng dụng React với tương tác phức tạp, quản lý trạng thái trở thành yếu tố then chốt để triển khai chức năng và duy trì mã nguồn. Quản lý trạng thái giúp chúng ta xử lý luồng dữ liệu, cung cấp một cơ chế quản lý trạng thái thống nhất cho ứng dụng, làm cho các thay đổi dữ liệu có thể dự đoán và dễ dàng theo dõi. Một hệ thống quản lý trạng thái tốt giúp cải thiện khả năng bảo trì, khả năng kiểm thử của ứng dụng và tăng cường hiệu quả làm việc của nhà phát triển.

Bản thân React là một thư viện nhẹ, chủ yếu chịu trách nhiệm về lớp hiển thị. Nó quản lý trạng thái nội bộ của các component thông qua propsstate. Tuy nhiên, khi cây component của ứng dụng sâu hơn và giao tiếp, chia sẻ trạng thái giữa các component trở nên phức tạp, việc chỉ dựa vào trạng thái nội bộ của component là không đủ. Lúc này, cần có một thư viện quản lý trạng thái bên ngoài để hỗ trợ, và Redux ra đời trong bối cảnh đó, trở thành một giải pháp phổ biến để quản lý trạng thái ứng dụng React. Chương này sẽ đi sâu vào lý do tại sao quản lý trạng thái hiệu quả lại quan trọng trong ứng dụng React và cách nó ảnh hưởng đến kiến trúc cũng như quy trình phát triển.

2. Khám Phá Các Khái Niệm Cốt Lõi Của Redux

2.1. Phân Tích Các Nguyên Lý Cơ Bản Của Redux

2.1.1. Vai Trò Và Cấu Trúc Của Store

Trong kiến trúc Redux, Store là nơi tập trung quản lý trạng thái của ứng dụng. Nó là một đối tượng JavaScript giữ tất cả trạng thái của ứng dụng và cung cấp một tập hợp các phương thức để truy cập và sửa đổi trạng thái. Mỗi ứng dụng Redux chỉ có duy nhất một store, được thiết kế để đảm bảo tính nhất quán và khả năng dự đoán của trạng thái.

Để tạo một store, chúng ta thường sử dụng hàm createStore, một API cốt lõi từ gói redux. Ví dụ:

import { createStore } from 'redux';
import mainReducer from './reducers'; // mainReducer là tổng hợp các reducer

const appStore = createStore(mainReducer);

Trong ví dụ này, createStore nhận một hàm reducer làm tham số, reducer này mô tả cách trạng thái ứng dụng thay đổi dựa trên các action khác nhau. mainReducer là tập hợp tất cả các hàm reducer, chúng cùng nhau quyết định trạng thái tổng thể của ứng dụng.

Cấu trúc chính của Store bao gồm:

  • state: Đối tượng bất biến lưu trữ trạng thái hiện tại của ứng dụng.
  • dispatch(action): Gửi một action đến reducer để cập nhật trạng thái.
  • subscribe(listener): Đăng ký một hàm callback sẽ được gọi khi trạng thái thay đổi.
  • getState(): Trả về một bản sao (snapshot) của trạng thái hiện tại.

Từ góc độ thiết kế phần mềm, store tuân thủ "mẫu Singleton", đảm bảo chỉ có một thể hiện trạng thái toàn cục và thể hiện này là chỉ đọc. Điều này bởi vì Redux khuyến khích chúng ta cập nhật trạng thái thông qua dữ liệu bất biến, nhằm tránh các vấn đề phát sinh từ việc sửa đổi trạng thái trực tiếp.

2.1.2. Các Nguyên Tắc Thiết Kế Của Reducers

Reducer là một hàm nhận trạng thái hiện tại (state) và một action làm tham số, sau đó trả về một trạng thái mới (newState). Trong Redux, tất cả các thay đổi trạng thái phải được thực hiện thông qua reducer. Do đó, reducer là cốt lõi của quản lý trạng thái Redux.

Các nguyên tắc thiết kế của Reducer:

  • Hàm thuần khiết (Pure Function): Reducer phải là một hàm thuần khiết, nghĩa là với cùng một đầu vào, nó luôn trả về cùng một đầu ra. Nó không gây ra bất kỳ tác dụng phụ nào và không phụ thuộc vào trạng thái bên ngoài.
  • Bất biến (Immutability): Khi xử lý cập nhật trạng thái, reducer không được trực tiếp sửa đổi trạng thái gốc. Thay vào đó, nó phải tạo và trả về một đối tượng trạng thái mới.
  • Phân nhánh (Branching): Tùy thuộc vào các kiểu action khác nhau, reducer có thể có các nhánh logic khác nhau.

Một reducer điển hình có thể trông như sau:

const initialItemState = { totalItems: 0 };

function itemCounterReducer(state = initialItemState, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, totalItems: state.totalItems + 1 };
    case 'REMOVE_ITEM':
      return { ...state, totalItems: state.totalItems - 1 };
    default:
      return state;
  }
}

Trong ví dụ này, itemCounterReducer cập nhật thuộc tính totalItems dựa trên các action khác nhau, nó luôn trả về một đối tượng trạng thái mới, đảm bảo tính bất biến của trạng thái. Bằng cách sử dụng toán tử spread (...), chúng ta tạo một bản sao nông của trạng thái gốc, thay vì sửa đổi đối tượng trạng thái gốc.

2.2. Cơ Chế Luân Chuyển Trạng Thái Của Redux

2.2.1. Định Nghĩa Và Phân Phối Actions

Trong Redux, action là một đối tượng JavaScript thông thường mô tả điều gì đã xảy ra. Nó là cách duy nhất để gửi dữ liệu từ các phần khác của ứng dụng đến store, có thể coi là "sự kiện" hoặc "thông điệp" trong hệ thống sự kiện.

Action định nghĩa đơn vị thông tin nhỏ nhất cần được reducer xử lý. Một đối tượng action chuẩn thường chứa các thuộc tính sau:

  • type: Một hằng số chuỗi, xác định loại hoạt động cần thực hiện.
  • payload: Một đối tượng thuộc bất kỳ kiểu nào, chứa dữ liệu bổ sung cần truyền cho reducer.

Ví dụ, một action điển hình có thể trông như sau:

const addItemAction = {
  type: 'ADD_ITEM',
  payload: { name: 'Sản phẩm mới', price: 100 }
};

Sau khi một action được định nghĩa, chúng ta có thể phân phối nó bằng cách gọi store.dispatch(), điều này sẽ kích hoạt việc thực thi hàm reducer và có thể dẫn đến cập nhật trạng thái.

appStore.dispatch(addItemAction);

Lời gọi đơn giản này là điểm khởi đầu của luân chuyển trạng thái Redux, nó sẽ tiếp tục kích hoạt logic reducer hiện có. Nếu reducer đó phản ứng với hoạt động ADD_ITEM, nó sẽ cập nhật trạng thái và trả về bản sao trạng thái mới.

2.2.2. Reducers Và Cập Nhật Trạng Thái

Khi một action được phân phối, store sẽ duyệt qua tất cả các hàm reducer đã đăng ký. Mỗi reducer sẽ quyết định xem có xử lý action này hay không dựa trên logic của riêng nó. Nếu xử lý, reducer sẽ trả về một đối tượng trạng thái mới; nếu không, nó trả về đối tượng trạng thái hiện tại.

function mainReducer(state = {}, action) {
  return {
    itemCounter: itemCounterReducer(state.itemCounter, action)
  };
}

Trong ví dụ này, mainReducer là một hàm reducer, nó gọi itemCounterReducer để xử lý logic liên quan đến bộ đếm mặt hàng. Ở đây, trạng thái được kết hợp bằng cách gán kết quả của itemCounterReducer cho thuộc tính itemCounter trong cây trạng thái. Bằng cách này, một ứng dụng có thể có một cấu trúc trạng thái lồng phức tạp, được quản lý bởi nhiều reducer cùng nhau.

2.2.3. Vai Trò Và Chức Năng Của Middlewares

Middleware trong Redux là một điểm mở rộng được chèn vào giữa phương thức dispatch của store và reducer. Nó cung cấp cơ hội để xử lý các action sắp được phân phối hoặc chặn giá trị trả về của reducer.

Các chức năng chính của Middleware bao gồm:

  • Ghi nhật ký (logging)
  • Thực hiện các cuộc gọi API bất đồng bộ
  • Báo cáo lỗi mã nguồn
  • Sửa đổi action
  • Cung cấp điểm tích hợp cho các middleware bên thứ ba (như redux-thunk, redux-saga, v.v.)

Middleware hoạt động thông qua một mô hình gọi chuỗi (chaining), mỗi middleware có thể truy cập vào middleware tiếp theo và store. Một ví dụ về middleware như sau:

const consoleLogger = (storeApi) => (nextActionHandler) => (action) => {
  console.log(`[LOGGER] Đang gửi hành động:`, action.type, action.payload || action.data);

  let result = nextActionHandler(action); // Chuyển action cho middleware tiếp theo hoặc reducer

  console.log(`[LOGGER] Trạng thái kế tiếp:`, storeApi.getState());

  return result;
}

Trong đoạn mã này, middleware consoleLogger nhận một storeApi, trả về một hàm, hàm này nhận middleware nextActionHandler, và sau đó trả về một hàm cuối cùng nhận action. Cách làm này đảm bảo mỗi middleware có thể truy cập vào API của store và được nối tiếp theo đúng thứ tự.

Trong ứng dụng thực tế, sử dụng applyMiddleware để áp dụng nhiều middleware vào store:

import { createStore, applyMiddleware } from 'redux';
import { consoleLogger } from './middleware/consoleLogger';
import mainReducer from './reducers';

const appStore = createStore(
  mainReducer,
  applyMiddleware(consoleLogger)
);

Điều này cho phép nhà phát triển chặn và xử lý action trước khi nó được gửi đến reducer, mở rộng đáng kể chức năng của Redux.

3. Sử Dụng react-redux Để Kết Nối Redux Với Component React

3.1. Triết Lý Thiết Kế Của react-redux

react-redux là thư viện liên kết React chính thức do Redux cung cấp, cho phép bạn truy cập Redux store trong các component React một cách khai báo, từ đó hiện thực hóa việc kết nối các component React với Redux store. Nó chủ yếu thông qua hai khái niệm quan trọng: Providerconnect, đơn giản hóa quy trình quản lý trạng thái trong ứng dụng React.

3.1.1. Vai Trò Của Component Provider

Component Provider là một phần rất quan trọng trong react-redux, nó giúp mọi component đều có thể tiếp cận được store, từ đó không cần phải truyền store như một prop một cách tường minh cho từng component con. Bằng cách truyền store làm thuộc tính cho component Provider, tất cả các component con có thể truy cập store thông qua hàm connect, từ đó hiện thực hóa việc truy cập toàn cục vào store trong cây component.

import { Provider } from 'react-redux';
import { createStore } from 'redux';
import MainApp from './MainApp';
import mainReducer from './reducers';
import ReactDOM from 'react-dom';

// Tạo instance store
const appStore = createStore(mainReducer);

// Bọc component cấp cao nhất của ứng dụng
ReactDOM.render(
  <Provider store={appStore}>
    <MainApp />
  </Provider>,
  document.getElementById('root')
);

Trong đoạn mã trên, chúng ta đầu tiên import component Provider và hàm createStore. Chúng ta bọc component cấp cao nhất MainApp bên ngoài Provider, và truyền store đã tạo làm thuộc tính. Bằng cách này, MainApp và tất cả các component con của nó đều có thể truy cập được appStore.

3.1.2. Cách Dùng Cơ Bản Của Hàm connect

Hàm connect là một khái niệm cốt lõi khác trong react-redux, được sử dụng để kết nối component React với Redux store. Đây là một hàm bậc cao (Higher-Order Function) nhận một component làm tham số và trả về một component mới. Component mới này sẽ đăng ký lắng nghe cập nhật của Redux store và truyền dữ liệu trạng thái mới dưới dạng props cho component được bọc. Hàm connect nhận hai tham số: mapStateToPropsmapDispatchToProps.

import { connect } from 'react-redux';

// Định nghĩa mapStateToProps, ánh xạ state vào props
const mapStateToComponentProps = state => ({
  productList: state.products.list,
  currentFilter: state.products.filter
});

// Định nghĩa mapDispatchToProps, ánh xạ dispatch vào props
const mapDispatchToComponentProps = dispatch => ({
  addProduct: (name) => dispatch({ type: 'ADD_PRODUCT', payload: { name } }),
  setFilter: (filterType) => dispatch({ type: 'SET_PRODUCT_FILTER', payload: { filterType } })
});

// Sử dụng connect để kết nối component
const ConnectedProductList = connect(
  mapStateToComponentProps,
  mapDispatchToComponentProps
)(ProductListDisplay);

export default ConnectedProductList;

Trong đoạn mã trên, chúng ta đã định nghĩa hai tham số mapStateToComponentPropsmapDispatchToComponentProps. Chúng ánh xạ trạng thái productListcurrentFilter từ Redux store vào các props của component ProductListDisplay, đồng thời liên kết hai actions addProductsetFilter với các phương thức trên props. Sau đó, chúng ta kết nối component ProductListDisplay với Redux store thông qua connect và tạo ra một component mới ConnectedProductList.

4. Cài Đặt Và Cấu Hình reduxreact-redux

4.1. Quản Lý Phụ Thuộc Với npm Và Yarn

4.1.1. Lựa Chọn Công Cụ Quản Lý Gói Thích Hợp

Trong phát triển frontend hiện đại, công cụ quản lý gói là một phần không thể thiếu. npm (Node Package Manager) và yarn là hai công cụ quản lý gói được sử dụng rộng rãi hiện nay. Mỗi công cụ đều có những ưu điểm riêng.

npm được cài đặt cùng với Node.js, là công cụ quản lý gói chính thức của Node.js. Nó có một kho gói khổng lồ, chức năng quản lý phụ thuộc mạnh mẽ, và tích hợp chặt chẽ với hệ sinh thái Node.js.

yarn là một công cụ quản lý gói thế hệ mới do Facebook, Google, Exponent và Tilde cùng phát triển, nhằm giải quyết một số nhược điểm của npm, như tốc độ cài đặt phụ thuộc chậm, quá trình cài đặt không ổn định. yarn có thể lưu trữ các gói đã tải xuống để tăng tốc độ cài đặt lặp lại, và sử dụng bộ nhớ cache offline để cải thiện độ tin cậy.

Đối với các dự án mới, bạn có thể chọn một trong hai làm công cụ quản lý gói cho dự án của mình. Thông thường, yarn được nhiều nhà phát triển ưu tiên vì hiệu suất vượt trội. Tuy nhiên, đối với các dự án hiện có đã sử dụng npm, không có động lực mạnh mẽ để chuyển đổi sang yarn, trừ khi có nhu cầu đặc biệt.

4.1.2. Cài Đặt redux Và react-redux

Sau khi xác định công cụ quản lý gói, bước tiếp theo là cài đặt reduxreact-redux. Hai thư viện này là bắt buộc để sử dụng Redux trong ứng dụng React. redux cung cấp chức năng cốt lõi của Redux, trong khi react-redux cung cấp liên kết giữa React và Redux.

npm install redux react-redux
# Hoặc sử dụng yarn
yarn add redux react-redux

Các lệnh trên sẽ cài đặt reduxreact-redux vào dự án của bạn, và tự động thêm chúng vào phần phụ thuộc trong tệp package.json.

4.2. Cấu Hình Redux Store

4.2.1. Tạo Instance Store

Cốt lõi của Redux là store, một container chịu trách nhiệm duy trì trạng thái ứng dụng và cung cấp các phương thức để lấy trạng thái, cập nhật trạng thái và lắng nghe thay đổi trạng thái. Mỗi ứng dụng Redux chỉ có một store duy nhất.

Việc tạo store trong dự án thường được thực hiện trong tệp store.js nằm trong thư mục src. Dưới đây là một ví dụ cơ bản về tạo store:

import { createStore } from 'redux';
import rootReducer from './reducers'; // Giả sử có một tệp reducers.js định nghĩa rootReducer

const applicationStore = createStore(rootReducer);

export default applicationStore;

Ở đây, hàm createStore được sử dụng để tạo store, nó chấp nhận một reducer làm tham số. rootReducer là một hàm trả về trạng thái mới dựa trên trạng thái hiện tại và hành động được kích hoạt. Trong ứng dụng thực tế, rootReducer có thể là nhiều reducer được kết hợp lại thông qua hàm combineReducers.

4.2.2. Tích Hợp Middleware Để Tăng Cường Chức Năng Của Store

Middleware cung cấp các chức năng mở rộng cho store, có thể được sử dụng để xử lý các hoạt động bất đồng bộ, ghi nhật ký, gọi API bên ngoài, v.v. Một middleware phổ biến là redux-thunk, nó cho phép chúng ta viết các action creators trả về hàm thay vì đối tượng, từ đó có thể thực hiện logic bất đồng bộ trong hàm.

Cài đặt redux-thunk:

npm install redux-thunk
# Hoặc sử dụng yarn
yarn add redux-thunk

Sau đó, áp dụng middleware này khi tạo store:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const applicationStore = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

export default applicationStore;

Thông qua hàm applyMiddleware, bạn có thể áp dụng một hoặc nhiều middleware vào store. Trong đoạn mã trên, chúng ta chỉ áp dụng một middleware thunk. Khi áp dụng nhiều middleware, chỉ cần truyền chúng làm tham số cho hàm applyMiddleware.

Đến đây, việc cài đặt và cấu hình Redux cũng như react-redux đã hoàn tất.

5. Triển Khai Actions Và Reducers Để Quản Lý Thay Đổi Trạng Thái

5.1. Thiết Kế Actions Hiệu Quả

5.1.1. Định Nghĩa Action Types

Trong kiến trúc Redux, Action Types là các hằng số được sử dụng để biểu thị các loại sự kiện khác nhau. Chúng thường được định nghĩa trong một tệp riêng biệt, chẳng hạn như actionTypes.js, để có thể được nhiều tệp nhập và sử dụng. Việc định nghĩa Action Types giúp giảm thiểu lỗi mã hóa cứng và cung cấp một tiêu chuẩn mã nguồn thống nhất trong dự án.

// actionTypes.js
export const FETCH_PRODUCT_REQUEST = 'FETCH_PRODUCT_REQUEST';
export const FETCH_PRODUCT_SUCCESS = 'FETCH_PRODUCT_SUCCESS';
export const ADD_NEW_PRODUCT = 'ADD_NEW_PRODUCT';

Trong ví dụ trên, chúng ta đã định nghĩa ba Action Types đơn giản: FETCH_PRODUCT_REQUEST, FETCH_PRODUCT_SUCCESSADD_NEW_PRODUCT. Chúng thường được viết hoa và sử dụng dấu gạch dưới để phân tách các từ. Khi Action Types được định nghĩa dưới dạng hằng số, bạn có thể dễ dàng sử dụng chúng trong toàn bộ ứng dụng, và khi cần thay đổi, chỉ cần sửa đổi ở một nơi duy nhất.

5.1.2. Tạo Và Sử Dụng Action Creators

Action Creators là các hàm định nghĩa và trả về một action. Chúng là mô tả về hành động sắp xảy ra và cung cấp cách để kích hoạt cập nhật trạng thái. Một Action Creator điển hình có thể trông như sau:

// actions/productActions.js
import { FETCH_PRODUCT_REQUEST, FETCH_PRODUCT_SUCCESS, ADD_NEW_PRODUCT } from '../actionTypes';

export function requestProducts() {
  return {
    type: FETCH_PRODUCT_REQUEST
  };
}

export function receiveProducts(productsData) {
  return {
    type: FETCH_PRODUCT_SUCCESS,
    payload: productsData
  };
}

export function createProduct(productDetails) {
  return {
    type: ADD_NEW_PRODUCT,
    payload: productDetails
  };
}

Trong đoạn mã trên, requestProducts, receiveProductscreateProduct đều là Action Creators. Chúng lần lượt trả về các đối tượng action với các kiểu khác nhau, chứa thuộc tính type và dữ liệu payload khác. Action Creators có thể là đồng bộ hoặc bất đồng bộ. Nếu cần xử lý logic bất đồng bộ, chúng ta có thể sử dụng middleware.

Khi sử dụng, bạn có thể gọi Action Creators như sau:

import { requestProducts, createProduct } from './actions/productActions';

// Sử dụng dispatch để gửi action đến reducer
dispatch(requestProducts());
dispatch(createProduct({ id: 'P001', name: 'Laptop', price: 1200 }));

5.2. Xây Dựng Reducers Để Xử Lý Trạng Thái

5.2.1. Cấu Trúc Cơ Bản Của Reducer

Trong Redux, Reducer là một hàm thuần khiết, nó nhận state hiện tại và một action, sau đó trả về state mới. Reducer không nên trực tiếp sửa đổi state gốc, mà thay vào đó phải trả về một instance state mới. Dưới đây là cấu trúc của một hàm reducer đơn giản:

// reducers/productReducer.js
import { FETCH_PRODUCT_REQUEST, FETCH_PRODUCT_SUCCESS, ADD_NEW_PRODUCT } from '../actionTypes';

const initialProductState = {
  loading: false,
  items: [],
  error: null
};

function productReducer(state = initialProductState, action) {
  switch (action.type) {
    case FETCH_PRODUCT_REQUEST:
      return { ...state, loading: true, error: null };
    case FETCH_PRODUCT_SUCCESS:
      return { ...state, loading: false, items: action.payload };
    case ADD_NEW_PRODUCT:
      return { ...state, items: [...state.items, action.payload] };
    default:
      return state;
  }
}

export default productReducer;

Trong ví dụ này, hàm productReducer kiểm tra type của action được truyền vào và trả về trạng thái khác nhau tùy thuộc vào loại action. Lưu ý, chúng ta đã sử dụng toán tử spread (...) để tạo các đối tượng trạng thái mới, đây là một cách tốt để duy trì tính bất biến của trạng thái.

5.2.2. Xử Lý Các Nhánh Logic Với Action Khác Nhau

Trong ứng dụng thực tế, reducer có thể trở nên khá phức tạp, đặc biệt khi đối tượng trạng thái lớn hoặc có nhiều loại action khác nhau cần xử lý. Để duy trì khả năng bảo trì của reducer, cách tốt nhất là phân tách reducer thành các hàm nhỏ hơn, mỗi hàm chịu trách nhiệm xử lý một loại action cụ thể:

// reducers/productReducer.js
import { FETCH_PRODUCT_REQUEST, FETCH_PRODUCT_SUCCESS, ADD_NEW_PRODUCT } from '../actionTypes';

function handleRequest(state) {
  return { ...state, loading: true, error: null };
}

function handleSuccess(state, payload) {
  return { ...state, loading: false, items: payload };
}

function handleAdd(state, payload) {
  return { ...state, items: [...state.items, payload] };
}

const initialProductState = {
  loading: false,
  items: [],
  error: null
};

function productReducer(state = initialProductState, action) {
  switch (action.type) {
    case FETCH_PRODUCT_REQUEST:
      return handleRequest(state);
    case FETCH_PRODUCT_SUCCESS:
      return handleSuccess(state, action.payload);
    case ADD_NEW_PRODUCT:
      return handleAdd(state, action.payload);
    default:
      return state;
  }
}

export default productReducer;

Trong đoạn mã trên, chúng ta đã tạo hai hàm trợ giúp handleRequest, handleSuccesshandleAdd để xử lý logic action cụ thể. Sau đó, chúng được gọi trong hàm reducer chính. Điều này giúp mỗi logic trở nên rõ ràng và tập trung hơn, đồng thời cũng giúp việc kiểm thử đơn vị dễ dàng hơn.

Hãy nhớ rằng, mỗi action phải được reducer trả về một trạng thái mới, không sửa đổi trạng thái gốc. Đây là chìa khóa để giữ cho ứng dụng có thể dự đoán và ổn định.

6. Áp Dụng Middleware Như redux-thunk Để Hỗ Trợ Các Thao Tác Bất Đồng Bộ

6.1. Khái Niệm Và Tác Dụng Của Middleware

6.1.1. Vai Trò Của Middleware Trong Redux

Trong kiến trúc Redux, Middleware (phần mềm trung gian) là một chuỗi các hàm cung cấp một điểm mở rộng trước khi một action được phân phối đến reducer. Chúng được sử dụng để thực hiện nhiều loại hoạt động khác nhau, như ghi nhật ký, gọi giao diện bất đồng bộ, định tuyến, v.v. Trong việc xử lý các hoạt động bất đồng bộ, middleware đặc biệt quan trọng vì nó có thể chèn logic tùy chỉnh giữa action và reducer.

Thứ tự thực thi của middleware tuân theo thứ tự chúng được áp dụng vào store. Mỗi middleware có cơ hội xử lý action được phân phối hoặc gọi middleware tiếp theo, hoặc truyền trực tiếp đến reducer. Thiết kế này làm cho middleware trở thành một công cụ mạnh mẽ và linh hoạt.

6.1.2. Tầm Quan Trọng Của Các Thao Tác Bất Đồng Bộ Trong Frontend

Trong phát triển frontend hiện đại, các thao tác bất đồng bộ có mặt ở khắp mọi nơi. Ứng dụng client cần giao tiếp với máy chủ, lấy dữ liệu, gửi dữ liệu hoặc tương tác với API của bên thứ ba. Các thao tác này đều là bất đồng bộ vì chúng thường liên quan đến yêu cầu mạng, mất thời gian không thể đoán trước để hoàn thành.

Nếu thực hiện trực tiếp các yêu cầu mạng trong component, nó sẽ vi phạm nguyên tắc luồng dữ liệu một chiều của Redux và làm cho logic ứng dụng trở nên khó theo dõi và kiểm thử. Bằng cách sử dụng middleware, chúng ta có thể xử lý các thao tác bất đồng bộ này một cách thanh lịch giữa action và reducer, giữ cho component sạch sẽ và dễ bảo trì.

6.2. Cài Đặt Và Sử Dụng redux-thunk

6.2.1. Nguyên Lý Hoạt Động Của redux-thunk

redux-thunk là một middleware đơn giản cho phép bạn viết các action creators trả về hàm, thay vì trả về một đối tượng thông thường. Hàm này nhận hai tham số: dispatchgetState, trong đó dispatch được sử dụng để gửi các action mới, và getState được sử dụng để truy cập trạng thái hiện tại.

Khi sử dụng redux-thunk, action creators có thể là các hàm trả về một hàm. Nếu action creator trả về một hàm, hàm này sẽ được gọi và truyền dispatchgetState làm tham số. Điều này cho phép bạn thực hiện các điều kiện, yêu cầu bất đồng bộ hoặc logic xử lý khác bên trong hàm tùy theo nhu cầu, sau đó quyết định có gửi action mới hay không.

6.2.2. Viết Các Action Creators Bất Đồng Bộ

Để sử dụng redux-thunk để viết các action creators bất đồng bộ, trước tiên bạn cần cài đặt redux-thunk:

npm install redux-thunk

Hoặc sử dụng yarn:

yarn add redux-thunk

Sau khi cài đặt, áp dụng redux-thunk trong cấu hình store:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const appStore = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

Bây giờ, bạn có thể tạo các action creators bất đồng bộ. Ví dụ, giả sử bạn muốn lấy dữ liệu sản phẩm từ máy chủ một cách bất đồng bộ sau khi một hành động nhất định xảy ra:

import { FETCH_PRODUCT_REQUEST, FETCH_PRODUCT_SUCCESS, FETCH_PRODUCT_FAILURE } from '../actionTypes';

function fetchProductData() {
  return async dispatch => {
    dispatch({ type: FETCH_PRODUCT_REQUEST }); // Gửi action báo hiệu bắt đầu yêu cầu

    try {
      const response = await fetch('https://api.example.com/products');
      const data = await response.json();
      dispatch({ type: FETCH_PRODUCT_SUCCESS, payload: data }); // Gửi action thành công với dữ liệu
    } catch (error) {
      dispatch({ type: FETCH_PRODUCT_FAILURE, error: error.message }); // Gửi action thất bại nếu có lỗi
    }
  };
}

Trong đoạn mã trên, fetchProductData là một action creator bất đồng bộ, nó khởi tạo một yêu cầu mạng. Nếu yêu cầu thành công, nó gửi một action FETCH_PRODUCT_SUCCESS với dữ liệu; nếu yêu cầu thất bại, nó gửi một action FETCH_PRODUCT_FAILURE. Sử dụng redux-thunk để gói gọn logic bất đồng bộ trong các action creators giúp tách biệt component khỏi logic quản lý trạng thái, giữ cho component UI đơn giản và dễ quản lý, kiểm thử.

7. Liên Kết Redux Store Với Component React Sử Dụng Hàm connect

7.1. Sử Dụng connect Để Kết Nối Component

connect là một hàm bậc cao (Higher-Order Function) được cung cấp bởi thư viện react-redux, nó có khả năng liên kết component React với store của Redux, giúp component có thể truy cập trạng thái trong store và có thể kích hoạt các thay đổi trạng thái.

7.1.1. Phân Tích Các Tham Số Của connect

Hàm connect về cơ bản nhận hai tham số: một hàm ánh xạ, được gọi là mapStateToProps, chịu trách nhiệm ánh xạ trạng thái từ store vào các props của component; tham số còn lại là một hàm ánh xạ, được gọi là mapDispatchToProps, chịu trách nhiệm ánh xạ phương thức dispatch của store vào các props của component.

function connectComponentToStore(mapState, mapDispatch){
  return function wrapComponent(ComponentToWrap){
    return class ConnectedComponent extends React.Component {
      // Logic kết nối và lắng nghe store
    }
  }
}
// connect được sử dụng như:
// export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
7.1.2. Ánh Xạ State Vào Props

Hàm mapStateToProps nên trả về một đối tượng, các thuộc tính của đối tượng này sẽ được hợp nhất vào các props của component. Khi trạng thái của Redux store được cập nhật, nếu đối tượng mới trả về khác với đối tượng cũ, component sẽ được render lại.

const mapInventoryStateToComponentProps = state => {
  return {
    inventoryItems: state.inventory.items,
    displayStatusFilter: state.inventory.filter
  };
};

7.2. Phân Tách Container Component Và Presentational Component

Trong React, component có thể được chia thành hai loại: Container Components (component chứa) và Presentational Components (component trình bày). Trách nhiệm chính của container component là xử lý dữ liệu và logic, trong khi presentational component chủ yếu chịu trách nhiệm hiển thị UI và kiểu dáng.

7.2.1. Trách Nhiệm Và Triển Khai Của Container Component

Container component thường là các component được sử dụng để kết nối với Redux store, chúng không cần quan tâm đến chi tiết kiểu dáng và trình bày, chỉ cần tập trung vào việc lấy dữ liệu và cập nhật trạng thái.

const InventoryListContainer = connect(
  mapInventoryStateToComponentProps,
  mapDispatchToComponentProps // giả sử đã định nghĩa trước
)(InventoryListDisplay);

// InventoryListDisplay là presentational component
7.2.2. Nguyên Tắc Phân Tách Giữa Presentational Component Và Container Component

Presentational component nên cố gắng không quan tâm đến cách lấy dữ liệu, chúng chỉ nhận dữ liệu dưới dạng props và trả về các phần tử React để mô tả cấu trúc UI. Sự phân tách này giúp component dễ bảo trì và kiểm thử hơn.

const InventoryListDisplay = ({ inventoryItems, displayStatusFilter, onItemClick }) => (
  <ul>
    {inventoryItems
      .filter(item => {
        if (displayStatusFilter === 'ALL') {
          return true;
        }
        return item.status === displayStatusFilter;
      })
      .map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name} ({item.status})
        </li>
      ))}
  </ul>
);

Để dễ hiểu hơn, dưới đây là một sơ đồ luồng đơn giản, minh họa cách hàm connect hoạt động:

Luồng hoạt động của connect:

  1. Redux Store chứa toàn bộ trạng thái của ứng dụng.
  2. Hàm mapStateToProps lấy dữ liệu từ Redux Store và ánh xạ thành props cho component.
  3. Hàm mapDispatchToProps tạo ra các hàm dispatch cho Redux Store và ánh xạ thành props cho component.
  4. Hàm connect sử dụng mapStateToPropsmapDispatchToProps để tạo ra một Container Component.
  5. Container Component nhận props từ connect và truyền chúng xuống cho Presentational Component.
  6. Presentational Component chỉ nhận props và hiển thị UI dựa trên dữ liệu đó.

Trong các dự án thực tế, bạn cần cân nhắc cách thiết kế state và actions để phù hợp với nhu cầu của component, làm cho mã nguồn vừa dễ bảo trì vừa có khả năng đọc tốt. Điều này thường có nghĩa là bạn cần thiết kế kỹ lưỡng cấu trúc dữ liệu và logic.

Thẻ: react redux State Management react-redux middleware

Đăng vào ngày 18 tháng 6 lúc 19:55