Trong bài viết này, chúng ta sẽ tìm hiểu về các nguyên tắc cơ bản của Redux và cách sử dụng Redux trong ứng dụng React. Sau đó, chúng ta sẽ xây dựng một ứng dụng đơn giản sử dụng Redux để giúp hiểu rõ hơn và đơn giản hóa việc sử dụng Redux.

Redux là gì?

Redux là một thư viện quản lý State (trạng thái) mã nguồn mở và chạy trên phía client, tạo ra một store toàn cục trong đó State có thể được chia sẻ giữa các component mà không cần thiết phải có kết nối cha-con (Truyền từ cha xuống con).

Trước khi tiếp tục với Redux, hãy hiểu về khái niệm State (trạng thái).

State là gì ?

State là một đối tượng JavaScript được sử dụng để lưu trữ các dữ liệu của một component. Khi state của một component thay đổi, React sẽ tự động re-render lại component để phản ánh những thay đổi này trên giao diện người dùng.

Ví dụ:

const state= {name: 'Nguyen Van A', age: '20'}

Khi nào thì nên sử dụng Redux?

  • State được truyền dưới dạng prop cho nhiều hơn hai component hoặc các cấp độ của các component.

  • Cùng một state được sử dụng bởi các component không có mối quan hệ cha-con.

  • Lưu trữ tạm state trang. Ví dụ - khi người dùng điều chỉnh trang tin tức và quay lại trang tin tức cũ, trang tin tức cũ vẫn hoạt động ở trạng thái như ban đầu.

  • Khi state thay đổi, các component liên quan đến state có thể được thay đổi đồng thời.

Kiến trúc Redux

Ba thành phần của Redux là:

  • Action
  • Reducer
  • Store

Action

Action là một hàm đơn giản trả về một đối tượng và có trách nhiệm gửi dữ liệu đến kho lưu trữ (store) thông qua hàm dispatchreducer.

Nó có hai thuộc tính - payload và type. Hành động được gọi là “type” và giá trị được áp dụng để thay đổi trạng thái của ứng dụng được gọi là “payload”.

Ví dụ:

const addItemAction = {
   type: 'ADD_ITEM',
   payload: {
      name: 'Milk',
      quantity: 1
   }
}

dispatch(addItemAction);

Reducer

“Reducer” là một hàm được sử dụng để thay đổi trạng thái của ứng dụng theo cách phụ thuộc vào các loại action cụ thể. Hàm nhận hai đối số: trạng thái hiện tại và action, và trả về một trạng thái mới. Chức năng chính của reducer là tạo ra một trạng thái mới cho store.

Ví dụ:

// Định nghĩa reducer để xử lý các action liên quan đến số lượng
const counterReducer = (state = initialState, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      };
    default:
      return state;
  }
}

Store

Store là một đối tượng JavaScript và là một kho chứa trung tâm quản lý trạng thái, có thể truy cập từ toàn bộ ứng dụng.

Phương thức duy nhất để truy cập vào store là thông qua việc gửi action.

Ví dụ:

import { createStore } from 'redux';

function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

const store = createStore(counter);

store.subscribe(() => console.log(store.getState()))

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
  • dispatch(action): Gửi một action đến Redux store để xử lý.

  • getState(): Trả về state hiện tại của Redux store.

  • subscribe(listener): Đăng ký một hàm lắng nghe để được gọi mỗi khi state của Redux thay đổi.

Redux Toolkit

Redux Toolkit là một thư viện hỗ trợ cho Redux, cung cấp các hàm API để giảm bớt boilerplate code trong các ứng dụng Redux. Các hàm này được trừu tượng hoá từ các hàm API hiện có của Redux và không làm thay đổi luồng xử lý của Redux.

Trong Redux Toolkit, các hàm chính bao gồm:

  • configureStore(): Hàm này được sử dụng để cấu hình global store của ứng dụng.
  • createAction(): Hàm này được sử dụng để tạo ra các action creators đơn giản hơn.
  • createReducer(): Hàm này được sử dụng để tạo reducers đơn giản hơn.
  • createSlice(): Hàm này tự động tạo ra các action creator và reducer dựa trên một slice (phần) của state.
  • createAsyncThunk(): Hàm này được sử dụng để tạo ra các async thunks trong Redux Toolkit.
  • createEntityAdapter(): Hàm này được sử dụng để xử lý dữ liệu thực thể (entity data) trong Redux Toolkit.

Sử dụng các hàm API này giúp giảm thiểu số lượng code cần phải viết trong ứng dụng Redux, nhất là các hàm createAction() và createReducer(). Nếu muốn đơn giản hóa hơn nữa, ta có thể sử dụng hàm createSlice(), mà tự động tạo ra các action creator và reducer dựa trên một slice của state.

Demo sử dụng Redux trong React

Phát triển một ứng dụng calculator đơn giản trong React sử dụng Redux.

Bước 1: Cài đặt các thư viện cần thiết

Tạo một ứng dụng React và cài đặt các thư viện Redux: react-redux và @reduxjs/toolkit.

npx create-react-app calculator
cd calculator
npm install react-redux @reduxjs/toolkit 

Bước 2: Tạo các Actions và Reducers

Có 2 cách để tạo các hành động và reducers:

  • C1: Cách truyền thống (Tách riêng Actions và Reducer)
  • C2: Sử dụng hàm createSlice của Redux Toolkit

Trước tiên xem xét cách reducersactions xuất hiện trong các ứng dụng React truyền thống

Tạo một tệp mới cho hàm reducer, để thay đổi giá trị của state dựa trên loại (type) của action.

CalculatorAction.js

export const increment = (value) => (dispatch) => {
  dispatch({ type: 'ADD', payload: value });
}

export const decrement = (value) => (dispatch) => {
  dispatch({ type: 'SUBTRACT', payload: value });
}

Cách viết trên tương dương với

export function increment(value) {
  return {
    type: 'ADD',
    payload: value
  }
}

export function decrement(value) {
  return {
    type: 'SUBTRACT',
    payload: value
  }
}

Sau đó, tạo một file mới để viết hàm reducer, với mục đích thay đổi giá trị của state dựa trên loại action được truyền vào.

CalculatorReducer.js

export const calculatorReducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    case 'SUBTRACT':
      return state - action.payload;
    default:
      return state;
  }
}

Trong đoạn mã trên:

  • Hàm calculatorReducer có hai tham số là state và action.
  • Đối với hàm này, chúng ta sử dụng câu lệnh switch để xác định giá trị của state dựa trên loại action được truyền vào.

Các trường hợp trong câu lệnh switch tương ứng với các loại action khác nhau mà chúng ta có thể gửi đến reducer.

Ví dụ, nếu loại action là ‘ADD’, reducer sẽ trả về một object mới bằng cách sao chép state hiện tại và cộng giá trị payload (giá trị được truyền vào action) vào thuộc tính result. Tương tự, nếu loại action là ‘SUBTRACT’, reducer sẽ trả về một object mới bằng cách sao chép state hiện tại và trừ giá trị payload khỏi thuộc tính result. Cuối cùng, nếu không có loại action nào phù hợp, reducer sẽ trả về state hiện tại mà không thay đổi gì.

Bây giờ chúng ta hãy xem cách sử dụng createSlice để đơn giản hóa quá trình và hoàn thành cùng chức năng như trên.

Tạo một tệp mới để khởi tạo giá trị ban đầu của state và các reducer function để thực hiện các action và thay đổi giá trị của state.

CalculatorSlice.js

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

const calculatorSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  }, reducers: {
    increment: (state, action) => {
      state.value += action.payload;
    },
    decrement: (state, action) => {
      state.value -= action.payload;
    }
  }
});

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

Như bạn có thể thấy từ đoạn mã trên, tất cả các action và reducer đã được sử dụng trong cùng một vị trí, khác với một ứng dụng Redux thông thường nơi mỗi action phải được quản lý kèm với reducer tương ứng. Sử dụng createSlice giúp loại bỏ yêu cầu switch case để chỉ định action.

Bằng cách định nghĩa tất cả các reducer function và các action creator trong cùng một đối tượng createSlice, chúng ta có thể tăng tính tổ chức của code và giảm thiểu số lượng code phải viết. Việc này cũng giúp kiểm soát tốt hơn và tránh nhầm lẫn trong code.

Bước 3: Cấu hình Redux Store trong ứng dụng.

Nhập các module sau:

  • configureStore từ thư viện @reduxjs/toolkit
  • Provider từ thư viện react-redux

Trong component ở cấp cao nhất của ứng dụng, để kết nối calculatorReducer với ứng dụng, chúng ta sẽ sử dụng các module này.

App.js

import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import Calculator from "./Calculator";
import calculatorReducer from './CalculatorSlice';
import DisplayValue from "./DisplayValue";

const store = configureStore({
  reducer: {
    calculator: calculatorReducer,
  },
});

function App() {
  return (
    <Provider store={store}>
      <Calculator />
      <DisplayValue />
    </Provider>
  )
}

export default App;

Trong đoạn mã trên, hàm configureStore được sử dụng để tạo một Redux store cho các reducer trong ứng dụng. Redux store là nơi lưu trữ toàn bộ trạng thái của ứng dụng và cung cấp phương thức để truy cập và cập nhật trạng thái này.

Component Provider có vai trò là cung cấp Redux store cho toàn bộ ứng dụng thông qua prop store. Bằng cách làm như vậy, các component con của ứng dụng có thể truy cập đến Redux store và sử dụng dữ liệu trong đó để cập nhật giao diện hoặc xử lý các thao tác người dùng.

Bước 4: Sử dụng Redux trong thành phần bằng cách sử dụng hooks.

Tạo hai thành phần - Calculator và DisplayValue.

Calculator.js

import React from 'react'
import { useDispatch } from 'react-redux'
import { decrement, increment } from './CalculatorSlice';

const Calculator = () => {
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(increment(Math.floor(Math.random() * 5) + 1))}>
        Increment
      </button>
      <button onClick={() => dispatch(decrement(Math.floor(Math.random() * 5) + 1))}>
        Decrement
      </button>
    </div>
  )
}

export default Calculator

Trong đoạn mã trên, thành phần Calculator chỉ bao gồm hai nút, để tăng hoặc giảm giá trị. Chúng ta sử dụng hook useDispatch để lấy tham chiếu của hàm dispatch từ store.

Khi người dùng nhấn vào nút ‘Increment’ hoặc ‘Decrement’, đó sẽ gọi các hàm tương ứng (increment() hoặc decrement()) và gửi action đến store. Đồng thời, reducer cập nhật trạng thái mới dựa trên loại action này.

DisplayValue.js

import React from 'react'
import { useSelector } from 'react-redux'

const DisplayValue = () => {

  const value = useSelector((state) => state.calculator.value);
  return (
    <div>
      <h1>{value}</h1>
    </div>
  )
}

export default DisplayValue

Trong đoạn mã trên, đó là một thành phần trình bày và chỉ hiển thị giá trị. Ở đây, ta sử dụng hook useSelector để lấy trạng thái từ Redux Store.

Kết luận

Trong bài viết này, chúng ta đã tìm hiểu về Redux và cách sử dụng nó trong React. Chúng ta đã phát triển một ứng dụng Calculator đơn giản sử dụng Redux để quản lý trạng thái và cho phép các thành phần liên quan truy cập các giá trị đó.

Tuy nhiên, Redux không phải là giải pháp cho tất cả các dự án. Nó được sử dụng trong các trường hợp khi bạn cần quản lý trạng thái toàn cục của ứng dụng hoặc khi có nhiều thành phần liên quan đến nhau cần truy cập vào cùng một trạng thái.