React Roadmap: Chọn Công Cụ Quản Lý State Nào? Context vs Redux vs Recoil

Chào mừng các bạn quay trở lại với series React Roadmap! Sau khi đã cùng nhau khám phá những viên gạch nền tảng như Props và State, hiểu về vòng đời component, làm quen với useState và useEffect, và thậm chí là tạo custom hooks hay sử dụng useReducer cho state cục bộ, chúng ta bắt đầu chạm trán với một vấn đề lớn hơn khi ứng dụng React phình to: Quản lý state toàn cục (Global State).

Khi dữ liệu cần được chia sẻ giữa các component ở những cấp độ khác nhau trong cây component (mà không phải lúc nào cũng có quan hệ cha-con trực tiếp), việc “prop drilling” (truyền props qua nhiều tầng) trở nên cồng kềnh và khó bảo trì. Đó là lúc chúng ta cần đến các giải pháp quản lý state toàn cục mạnh mẽ hơn.

Trong bài viết này, chúng ta sẽ cùng nhau “giải mã” ba cái tên phổ biến và quyền lực trong thế giới quản lý state React: React Context API, Redux, và Recoil. Chúng ta sẽ tìm hiểu xem chúng là gì, hoạt động ra sao, điểm mạnh yếu thế nào, và quan trọng nhất là: khi nào nên chọn “người hùng” nào cho ứng dụng của bạn.

Tại sao lại cần Global State Management?

Như đã thảo luận trong bài viết về React Props vs State, useState hay useReducer rất tuyệt vời để quản lý state cục bộ (local state) bên trong một component hoặc giữa các component cha-con gần kề. Tuy nhiên, hãy tưởng tượng bạn có một state cần dùng ở component A, component B, và component C, mà chúng lại nằm rải rác ở các nhánh khác nhau của cây component.

Cách tiếp cận “truyền thống” của React là nâng state lên component cha chung gần nhất (lifting state up) và truyền state đó xuống các component con thông qua props. Nhưng nếu component cha chung đó ở rất xa, bạn sẽ phải truyền state qua hàng loạt các component “trung gian” không hề cần dùng đến state đó. Đây chính là “prop drilling”.

Prop drilling làm code trở nên:

  • Khó đọc: Rất khó để theo dõi dữ liệu đang đi đến đâu và từ đâu tới.
  • Khó bảo trì: Chỉ một thay đổi nhỏ về cấu trúc component hoặc nơi cần dùng state cũng có thể yêu cầu thay đổi ở rất nhiều file.
  • Khó tái cấu trúc (refactor): Việc sắp xếp lại các component trở nên phức tạp hơn.

Các thư viện/API quản lý state toàn cục ra đời để giải quyết vấn đề này, cung cấp một nơi tập trung để lưu trữ state và cho phép bất kỳ component nào (quan tâm) đều có thể truy cập và cập nhật state đó mà không cần truyền qua props.

Chúng ta đã có một bài riêng về Sử dụng useContext để Quản lý Global State. Bây giờ, hãy đặt Context API vào bức tranh tổng thể cùng với Redux và Recoil.

React Context API: Sự Lựa Chọn Tự Nhiên

React Context API là tính năng được tích hợp sẵn trong React từ phiên bản 16.3, cung cấp một cách để truyền dữ liệu qua cây component mà không cần truyền props thủ công qua mọi cấp độ. Nó được thiết kế cho các dữ liệu được coi là “global” đối với một cây component con, ví dụ như theme hiện tại, thông tin người dùng đã đăng nhập, ngôn ngữ ưa thích,…

Cách hoạt động cơ bản

Context hoạt động dựa trên hai thành phần chính:

  1. <strong>Context Object:</strong> Tạo bằng React.createContext(). Nó chứa một component Provider và một component Consumer (hoặc bạn dùng hook useContext).
  2. <strong>Provider:</strong> Component này bọc lấy phần cây component cần truy cập state. Nó nhận một prop value, đây chính là dữ liệu mà bạn muốn chia sẻ.
  3. <strong>Consumer (hoặc useContext hook):</strong> Component con ở bất kỳ đâu trong cây được bọc bởi Provider có thể “tiêu thụ” (truy cập) giá trị từ Context. Với hooks, bạn dùng const value = useContext(MyContext);.

Ví dụ đơn giản với useContext (như đã nói trong bài viết trước):

// theme-context.js
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null); // Giá trị mặc định

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

// App.js
import { ThemeProvider } from './theme-context';
import MyComponent from './MyComponent';

function App() {
  return (
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );
}

// MyComponent.js
import { useTheme } from './theme-context';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

Điểm mạnh của Context API:

  • Tích hợp sẵn: Không cần cài đặt thư viện bên ngoài.
  • Đơn giản: Dễ học và dễ sử dụng cho các trường hợp đơn giản.
  • Tuyệt vời cho dữ liệu “ít thay đổi”: Phù hợp với các giá trị ít khi thay đổi như theme, thông tin người dùng tĩnh.

Điểm yếu của Context API:

  • Hiệu năng khi dữ liệu thay đổi thường xuyên: Khi giá trị trong Provider thay đổi, *tất cả* các component con sử dụng Context đó (hoặc nằm dưới Provider) sẽ bị re-render, ngay cả khi chúng không trực tiếp sử dụng phần dữ liệu thay đổi. Điều này có thể gây ra vấn đề hiệu năng với các state phức tạp hoặc thay đổi liên tục. Có thể dùng useMemo hoặc tách nhỏ Context để mitigate, nhưng sẽ làm phức tạp code.
  • Thiếu cấu trúc: Context chỉ là một cơ chế truyền dữ liệu. Nó không cung cấp pattern hay cấu trúc cho việc quản lý state và logic nghiệp vụ (như Redux với actions/reducers). Khi state phức tạp, việc quản lý logic cập nhật trở nên khó khăn.
  • Khó debug: Thiếu các công cụ debug chuyên dụng như Redux DevTools.

Context API là một lựa chọn tốt cho các ứng dụng nhỏ đến trung bình hoặc cho các phần state đơn giản, ít thay đổi. Nếu bạn chỉ cần tránh prop drilling cho một vài giá trị đơn giản, Context là đủ.

Redux: Người Khổng Lồ Đáng Tin Cậy

Redux là một thư viện quản lý state phổ biến nhất cho React (và các thư viện UI khác). Nó không chỉ là một “container” chứa state, mà còn là một pattern (kiến trúc) giúp state của ứng dụng có thể dự đoán được (predictable). Triết lý của Redux dựa trên ba nguyên tắc cốt lõi:

  1. Single source of truth: Toàn bộ state của ứng dụng được lưu trữ trong một object duy nhất gọi là “store”.
  2. State is read-only: Cách duy nhất để thay đổi state là gửi (dispatch) một “action”.
  3. Changes are made with pure functions: Để chỉ định state thay đổi như thế nào dựa trên action, bạn viết các “reducers”. Reducers là các hàm thuần khiết (pure functions) nhận state hiện tại và action, rồi trả về state mới.

Flow dữ liệu trong Redux rất rõ ràng:

UI -> Dispatch Action -> Reducer -> New State -> Update UI

Để sử dụng Redux hiệu quả với React, bạn thường dùng thêm thư viện react-redux. Ngày nay, cách tiếp cận hiện đại và được khuyến khích là sử dụng Redux Toolkit (RTK). RTK giúp giảm đáng kể boilerplate code và đơn giản hóa việc cài đặt Redux.

Ví dụ về một slice (trong Redux Toolkit) cho counter:

// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit cho phép "mutate" state trực tiếp trong reducers
      // nhờ thư viện Immer, nhưng thực chất là tạo ra state mới immutable.
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// Export actions và reducer
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
// index.js hoặc App.js (entry point)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
// features/counter/CounterComponent.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';

export function CounterComponent() {
  // Lấy state từ store
  const count = useSelector((state) => state.counter.value);
  // Lấy hàm dispatch
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
        <button
          aria-label="Increment by 5"
          onClick={() => dispatch(incrementByAmount(5))}
        >
          Increment by 5
        </button>
      </div>
    </div>
  );
}

Điểm mạnh của Redux:

  • Khả năng dự đoán (Predictability): Luồng dữ liệu một chiều và việc thay đổi state thông qua các actions/reducers thuần khiết giúp state rất dễ đoán và kiểm soát.
  • Công cụ debug mạnh mẽ: Redux DevTools là một trong những công cụ debug state tốt nhất, cho phép “time-travel debugging” (quay ngược thời gian xem state thay đổi thế nào qua từng action).
  • Hệ sinh thái phong phú: Có rất nhiều thư viện mở rộng cho Redux (middleware) để xử lý side effects (như Redux Thunk, Redux Saga), tích hợp với router (Connected React Router),…
  • Cộng đồng lớn và ổn định: Là thư viện đã có từ lâu, có rất nhiều tài nguyên, tutorial và được sử dụng rộng rãi trong các ứng dụng lớn, phức tạp.
  • Quản lý side effects hiệu quả: Cung cấp các pattern rõ ràng để xử lý các tác vụ bất đồng bộ (như gọi API).

Điểm yếu của Redux:

  • Boilerplate: Mặc dù Redux Toolkit đã giảm thiểu đáng kể, Redux vẫn có thể yêu cầu nhiều code hơn so với Context hoặc Recoil, đặc biệt với các state đơn giản.
  • Learning Curve: Việc hiểu toàn bộ các khái niệm (actions, reducers, store, middleware, selectors) ban đầu có thể hơi khó khăn đối với người mới bắt đầu.
  • Có thể là overkill: Đối với các ứng dụng nhỏ hoặc chỉ cần quản lý một vài state global đơn giản, việc tích hợp và cấu hình Redux có thể tốn nhiều công sức hơn giá trị mang lại.

Redux là lựa chọn hàng đầu cho các ứng dụng React lớn, phức tạp, cần quản lý state quy mô lớn, có nhiều logic nghiệp vụ phức tạp, yêu cầu khả năng debug mạnh mẽ và xử lý side effects có cấu trúc.

Recoil: Cách Tiếp Cận “React-ish”

Recoil là một thư viện quản lý state mã nguồn mở được tạo bởi Facebook (Meta), đặc biệt dành cho các ứng dụng React. Triết lý của Recoil là cung cấp một giải pháp quản lý state đơn giản, linh hoạt và tương thích tốt với các tính năng mới của React như Suspense và Concurrent Mode.

Recoil tập trung vào việc định nghĩa các “đơn vị” state nhỏ, độc lập gọi là “atoms” và các hàm tính toán phái sinh từ state gọi là “selectors”.

Cách hoạt động cơ bản:

  1. <strong>Atoms:</strong> Là các đơn vị state có thể cập nhật và đăng ký (subscribe) theo dõi. Khi một atom thay đổi, chỉ các component đăng ký theo dõi atom đó mới re-render.
  2. <strong>Selectors:</strong> Là các hàm thuần khiết nhận atoms (hoặc các selectors khác) làm đầu vào và trả về một giá trị phái sinh. Selectors có thể được coi là “computed state” hoặc các truy vấn (queries) tới state. Chúng có thể bất đồng bộ (async).
  3. <strong>RecoilRoot:</strong> Tương tự Provider của Context/Redux, component này cần bọc lấy cây component sử dụng Recoil.

Ví dụ về atom và selector trong Recoil:

// state.js
import { atom, selector } from 'recoil';

export const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

export const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({ get }) => {
    const text = get(textState); // Lấy giá trị từ atom textState
    return text.length;
  },
});
// App.js
import React from 'react';
import { RecoilRoot } from 'recoil';
import TextInput from './TextInput';
import CharacterCount from './CharacterCount';

function App() {
  return (
    <RecoilRoot>
      <h1>Recoil Example</h1>
      <TextInput />
      <CharacterCount />
    </RecoilRoot>
  );
}

// TextInput.js
import React from 'react';
import { useRecoilState } from 'recoil';
import { textState } from './state';

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

// CharacterCount.js
import React from 'react';
import { useRecoilValue } from 'recoil';
import { charCountState } from './state';

function CharacterCount() {
  const count = useRecoilValue(charCountState); // Chỉ lấy giá trị, không cần setter

  return <>Character Count: {count}</>;
}

Điểm mạnh của Recoil:

  • Đơn giản và linh hoạt: Khái niệm atoms và selectors trực quan, dễ hiểu, đặc biệt quen thuộc với những ai đã dùng React Hooks.
  • Hiệu quả về hiệu năng: Recoil sử dụng mô hình đồ thị (graph) cho state. Khi một atom thay đổi, Recoil chỉ thông báo cho các component và selectors *thực sự* phụ thuộc vào atom đó, giúp tối ưu re-render so với Context API (khi một Provider thay đổi).
  • Tương thích tốt với React: Được thiết kế bởi nhóm phát triển React, Recoil tích hợp mượt mà với các tính năng của React như Suspense (để quản lý data fetching trong selectors bất đồng bộ) và Concurrent Mode.
  • Code “React-ish”: Các API như useRecoilState, useRecoilValue, useSetRecoilState trông rất giống với các React Hooks gốc.
  • State phân tán: State được định nghĩa thành các atom nhỏ, độc lập, dễ dàng chia sẻ và tái sử dụng ở bất kỳ đâu mà không cần cấu trúc tập trung phức tạp như Redux.

Điểm yếu của Recoil:

  • Mới hơn: So với Redux, Recoil là thư viện tương đối mới (ra mắt năm 2020, bản stable đầu tiên cuối năm 2022). Hệ sinh thái và cộng đồng còn nhỏ hơn.
  • Ít công cụ debug chuyên sâu: Mặc dù đã có Recoilize (một extension devtools), nó vẫn chưa mạnh mẽ và trưởng thành như Redux DevTools.
  • Chỉ dành cho React: Không thể sử dụng Recoil với các thư viện UI khác như Vue hay Angular. (Redux có thể dùng được).

Recoil là một lựa chọn đầy hứa hẹn, đặc biệt cho các ứng dụng React mới. Nó mang lại sự đơn giản của Context nhưng với hiệu năng tốt hơn và cấu trúc rõ ràng hơn cho state phức tạp hơn, đồng thời tích hợp tốt với tương lai của React.

So sánh Tổng quan: Context vs Redux vs Recoil

Để có cái nhìn rõ ràng hơn, hãy cùng so sánh ba công cụ này qua một vài tiêu chí:

Tiêu chí React Context API Redux (với Redux Toolkit) Recoil
Khái niệm cốt lõi Context, Provider, Consumer/useContext Store, Actions, Reducers, Middleware, Slices (RTK) Atoms, Selectors, RecoilRoot, Hooks (useRecoilState, useRecoilValue, useSetRecoilState)
Tích hợp Tích hợp sẵn trong React Thư viện ngoài (cần redux, react-redux, @reduxjs/toolkit) Thư viện ngoài (cần recoil)
Độ phức tạp / Learning Curve Thấp (cho các trường hợp đơn giản) Trung bình – Cao (với RTK đã giảm bớt nhiều so với Redux core) Thấp – Trung bình
Boilerplate Thấp (tăng khi cần xử lý hiệu năng/state phức tạp) Trung bình (đã giảm nhiều với RTK) Thấp
Hiệu năng Có thể kém khi state thay đổi thường xuyên (gây re-render rộng) Tốt (với selectors và tối ưu re-render) Tốt (chỉ re-render component phụ thuộc vào atom thay đổi)
Công cụ Debug Hạn chế Rất mạnh (Redux DevTools) Tốt (Recoilize DevTools, nhưng kém hơn Redux)
Quản lý Side Effects Cần quản lý thủ công hoặc kết hợp với useEffect/custom hooks. Có các middleware chuyên dụng (Thunk, Saga, Query,…) Có thể sử dụng selectors bất đồng bộ hoặc useEffect.
Tính linh hoạt (cấu trúc state) Hạn chế (chủ yếu là một giá trị duy nhất) Rigid (single store, schema tập trung) Rất linh hoạt (state phân tán theo atoms)
Ứng dụng phù hợp Nhỏ đến trung bình, state đơn giản, ít thay đổi. Lớn, phức tạp, cần quản lý state quy mô doanh nghiệp, logic phức tạp, debug chuyên sâu. Trung bình đến lớn, cần hiệu năng tốt, state phân tán, tích hợp với React features mới.
Mức độ trưởng thành/Cộng đồng Rất trưởng thành (tích hợp sẵn) Rất trưởng thành, cộng đồng lớn nhất Đang phát triển, cộng đồng nhỏ hơn Redux.

Khi nào chọn Công cụ nào?

Câu hỏi quan trọng nhất! Không có câu trả lời “một size vừa cho tất cả”. Lựa chọn phụ thuộc vào nhiều yếu tố:

  1. Quy mô và độ phức tạp của ứng dụng:
    • Ứng dụng nhỏ hoặc trung bình, state đơn giản, ít thay đổi: Context API là đủ và đơn giản nhất. Nó giải quyết tốt vấn đề prop drilling cơ bản.
    • Ứng dụng lớn, phức tạp, nhiều logic nghiệp vụ, state thay đổi thường xuyên: Redux (với Redux Toolkit) là lựa chọn truyền thống và mạnh mẽ. Nó mang lại cấu trúc, khả năng debug vượt trội và hệ sinh thái phong phú cho việc xử lý side effects phức tạp.
    • Ứng dụng trung bình đến lớn, cần hiệu năng tốt, state có thể được chia nhỏ và phân tán, muốn trải nghiệm “React-ish”: Recoil là một lựa chọn hiện đại, hiệu quả và dễ tiếp cận hơn Redux đối với nhiều developer React.
  2. Kinh nghiệm của team:
    • Nếu team đã quen thuộc với Redux và các pattern của nó, tiếp tục sử dụng Redux có thể là lựa chọn an toàn và hiệu quả.
    • Nếu team mới với quản lý state toàn cục và đang làm ứng dụng quy mô vừa, Recoil có thể có learning curve dễ chịu hơn Redux. Context là dễ nhất để bắt đầu nhưng sẽ gặp khó khăn khi scale.
  3. Yêu cầu cụ thể:
    • Bạn có cần time-travel debugging không? Redux là vô địch ở khoản này.
    • Bạn có cần xử lý side effects phức tạp (như chuỗi các hành động bất đồng bộ, cancel requests,…)? Hệ sinh thái middleware của Redux rất mạnh.
    • Bạn có quan tâm đến việc tận dụng tối đa các tính năng mới của React như Suspense không? Recoil có lợi thế ở đây.
    • Bạn có cần một giải pháp có thể dùng ngoài React không? Redux có thể, Context và Recoil thì không.

Một số lời khuyên:

  • Bắt đầu đơn giản: Đừng vội vàng nhảy vào Redux hoặc Recoil nếu bạn chỉ cần quản lý state theme hay user info cơ bản. Context API có thể là đủ.
  • Redux Toolkit is the standard: Nếu chọn Redux, hãy dùng Redux Toolkit. Đừng cố gắng cấu hình Redux core từ đầu trừ khi bạn có lý do rất đặc biệt.
  • Hãy thử Recoil: Nếu bạn đang bắt đầu một dự án React mới có quy mô từ trung bình trở lên và muốn một giải pháp hiện đại, “React-ish” và hiệu quả về hiệu năng, Recoil rất đáng để cân nhắc.
  • Có thể kết hợp: Không nhất thiết phải dùng chỉ một. Đôi khi, bạn có thể dùng Context cho các state ít thay đổi (theme, language) và Redux/Recoil cho state nghiệp vụ phức tạp hơn.

Lời kết

Quản lý state toàn cục là một bước quan trọng trong lộ trình phát triển ứng dụng React lên một tầm cao mới. React Context API, Redux, và Recoil đều là những công cụ mạnh mẽ, nhưng mỗi loại lại có triết lý, điểm mạnh, điểm yếu và các trường hợp sử dụng phù hợp riêng.

Thay vì tìm kiếm “công cụ tốt nhất”, hãy tập trung vào việc hiểu rõ nhu cầu của ứng dụng và kinh nghiệm của team để đưa ra lựa chọn phù hợp nhất tại thời điểm hiện tại. Công cụ tốt nhất là công cụ giúp bạn xây dựng ứng dụng hiệu quả, dễ bảo trì và mở rộng.

Hy vọng bài viết này đã cung cấp cho bạn một cái nhìn tổng quan và sâu sắc về ba lựa chọn phổ biến này. Việc nắm vững cách hoạt động và khi nào sử dụng từng loại sẽ giúp bạn đưa ra quyết định sáng suốt hơn trong các dự án thực tế.

Trong bài viết tiếp theo của series React Roadmap, chúng ta sẽ cùng nhau khám phá một khía cạnh quan trọng khác: Routing trong React với React Router. Hẹn gặp lại các bạn!

Chỉ mục