Facebook Pixel

State Management trong React: Context API, Redux, Recoil, React Query, Zustand

10 Oct, 2024

Tran Thuy Vy

Frontend Developer

Để giải quyết vấn đề Props Drilling trong React, bạn cần các giải pháp quản lý trạng thái toàn cục như: Context API, Redux, Recoil, React Query

State Management trong React: Context API, Redux, Recoil, React Query, Zustand

Mục Lục

Khi bạn build ứng dụng React, việc quản lý dữ liệu (state) giữa các thành phần là yếu tố then chốt giúp ứng dụng hoạt động mượt mà và dễ bảo trì. Đối với những ứng dụng nhỏ, bạn chỉ cần quản lý state cục bộ (local state) trong từng component là đủ.

Tuy nhiên, khi ứng dụng ngày càng phức tạp và có nhiều thành phần cần chia sẻ dữ liệu, việc quản lý state trở nên thách thức hơn, dẫn đến những vấn đề như "props drilling" – truyền props từ component cha qua nhiều cấp con khiến mã nguồn trở nên khó đọc.

Để giải quyết vấn đề này, các công cụ quản lý trạng thái toàn cục (global state management) như: Context API và Redux đã ra đời và trở thành những lựa chọn phổ biến. Ngoài ra, còn có các thư viện khác như Recoil, React Query, Zustand, và XState cung cấp những cách tiếp cận đa dạng và hiệu quả hơn.

Hãy cùng mình đi qua bài viết này để tìm hiểu về các giải pháp quản lý state phổ biến trong React và phân biệt rõ khi nào nên sử dụng.

1. State Management trong React

Đối với những ứng dụng nhỏ, bạn có thể chỉ cần quản lý state cục bộ trong từng component bằng cách sử dụng hook useState.

Tuy nhiên, khi ứng dụng lớn hơn và có nhiều thành phần cần chia sẻ dữ liệu, "props drilling" xuất hiện. Props drilling là hiện tượng bạn phải truyền dữ liệu từ component cha xuống qua nhiều cấp con để các component sâu trong cây có thể truy cập và sử dụng dữ liệu đó. Điều này làm cho mã nguồn trở nên rối và khó bảo trì.

Để giải quyết vấn đề này, bạn cần các giải pháp quản lý trạng thái toàn cục, cho phép lưu trữ và quản lý dữ liệu tại một nơi và các component có thể truy cập dữ liệu này một cách dễ dàng mà không cần truyền props.

2. Giới thiệu Context API

2.1 Context API là gì?

Context API là một công cụ có sẵn trong React để chia sẻ dữ liệu giữa các component mà không cần phải truyền qua props từng cấp. Bạn có thể hình dung nó như một nơi trung gian để lưu dữ liệu, và bất kỳ component nào trong cây cũng có thể truy cập vào dữ liệu này.

context api la gi

2.2 Khi nào nên sử dụng Context API?

Bạn nên sử dụng Context API khi:

  • Chỉ có ít dữ liệu cần chia sẻ giữa các component, ví dụ như theme, ngôn ngữ, hoặc trạng thái người dùng đã đăng nhập.
  • Bạn muốn tránh việc truyền props qua nhiều cấp (props drilling).

Ví dụ, nếu bạn muốn quản lý theme:

Typescript
const ThemeContext = React.createContext();

const App = () => {
  const [theme, setTheme] = React.useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <MainContent />
    </ThemeContext.Provider>
  );
};

const Header = () => {
  const { theme, setTheme } = React.useContext(ThemeContext);

  return (
    <header className={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Đổi màu nền
      </button>
    </header>
  );
};

2.3 Ưu, nhược điểm của Context API

Ưu điểm:

  • Đơn giản và tích hợp sẵn trong React, không cần cài đặt thêm thư viện.
  • Giảm props drilling.

Nhược điểm:

  • Không phù hợp với các ứng dụng lớn và phức tạp.
  • Mỗi khi state thay đổi, tất cả component sử dụng Context đều bị re-render, có thể ảnh hưởng đến hiệu suất, dẫn đến kết quả không mong muốn.

2.4 Hướng dẫn sử dụng Context API cho dự án React

B1: Tạo Context

  • Đầu tiên, bạn cần tạo file chứa Context để lưu trữ dữ liệu. Ví dụ, nếu bạn muốn quản lý trạng thái theme (giao diện sáng/tối), bạn có thể tạo một context như sau:
Typescript
import React, { createContext, useState, ReactNode } from "react";

interface ThemeContextType {
  theme: string;
  setTheme: (theme: string) => void;
}

export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const [theme, setTheme] = useState<string>("light");

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

B2: Bọc ứng dụng bằng Provider

Bọc ứng dụng với ThemeProvider để cung cấp context cho các component con.

Typescript
import React from "react";
import { ThemeProvider } from "./context/ThemeContext";
import Header from "./components/Header";
import MainContent from "./components/MainContent";

const App: React.FC = () => {
  return (
    <ThemeProvider>
      <Header />
      <MainContent />
    </ThemeProvider>
  );
};

export default App;

Bước 3: Truy cập vào dữ liệu trong các component con

• Trong các component con, bạn có thể truy cập dữ liệu từ Context bằng hook useContext.

Typescript
import React, { useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";

const Header: React.FC = () => {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error("ThemeContext must be used within a ThemeProvider");
  }

  const { theme, setTheme } = context;

  return (
    <header className={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        change theme
      </button>
    </header>
  );
};

export default Header;

3. Giới thiệu về Redux

3.1 Redux là gì?

Redux là thư viện quản lý trạng thái toàn cục, giúp bạn lưu trữ và quản lý trạng thái của toàn bộ ứng dụng một cách tập trung. Redux hoạt động:

  1. Store: chứa toàn bộ trạng thái của ứng dụng.
  2. Action: sự kiện yêu cầu thay đổi state.
  3. Reducer: hàm nhận vào state hiện tại và action, sau đó trả về state mới.

Redux cũng tuân thủ nguyên tắc "immutable state", nghĩa là trạng thái không thay đổi trực tiếp mà luôn tạo ra bản sao mới khi cần cập nhật.

redux là gì

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

Nên sử dụng Redux khi:

  • Ứng dụng phức tạp, nhiều component cần chia sẻ state và có nhiều logic cập nhật trạng thái.
  • Bạn cần quản lý nhiều loại trạng thái, ví dụ: giỏ hàng, trạng thái người dùng, hoặc dữ liệu sản phẩm.
  • Bạn muốn có một công cụ quản lý state mạnh mẽ và dễ mở rộng.

Ví dụ quản lý danh sách TODO với Redux:

Typescript
const addTodo = (todo) => {
  return {
    type: 'ADD_TODO',
    payload: todo,
  };
};

const todoReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    default:
      return state;
  }
};

const store = Redux.createStore(todoReducer);

const TodoApp = () => {
  const todos = store.getState();
  const [newTodo, setNewTodo] = React.useState("");

  const handleAddTodo = () => {
    store.dispatch(addTodo(newTodo));
    setNewTodo("");
  };

  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button onClick={handleAddTodo}>Add Todo</button>
    </div>
  );
};

3.3 Ưu, nhược điểm của Redux

Ưu điểm:

  • Quản lý state tập trung và rõ ràng, phù hợp với các ứng dụng lớn.
  • Công cụ debug như Redux DevTools giúp bạn dễ dàng theo dõi trạng thái.
  • Có thể dễ dàng mở rộng khi ứng dụng phát triển.

Nhược điểm:

  • Cấu hình phức tạp hơn Context API, đặc biệt với những người mới bắt đầu.
  • Mã nguồn có thể trở nên cồng kềnh nếu dùng cho các ứng dụng nhỏ.

3.4 Hướng dẫn sử dụng Redux trong dự án React

Bước 1: Cài đặt Redux và các thư viện liên quan

Bash
npm install redux react-redux @types/react-redux

Bước 2: Tạo action và reducer với TypeScript

  • Định nghĩa các kiểu dữ liệu cho action và state trong Redux.
  • Tạo action và reducer:
Typescript
export interface Todo {
  id: number;
  text: string;
}

export interface AddTodoAction {
  type: "ADD_TODO";
  payload: Todo;
}

export type TodoActionTypes = AddTodoAction;
Typescript
import { Todo, AddTodoAction } from "../types/todo";

export const addTodo = (todo: Todo): AddTodoAction => {
  return {
    type: "ADD_TODO",
    payload: todo,
  };
};


import { Todo, TodoActionTypes } from "../types/todo";

const initialState: Todo[] = [];

export const todoReducer = (
  state = initialState,
  action: TodoActionTypes
): Todo[] => {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, action.payload];
    default:
      return state;
  }
};

B3: Tạo Store

Kết hợp các reducer và tạo store với Redux:

Typescript
import { createStore, combineReducers } from "redux";
import { todoReducer } from "../reducers/todoReducer";

const rootReducer = combineReducers({
  todos: todoReducer,
});

export const store = createStore(rootReducer);

export type RootState = ReturnType<typeof rootReducer>;

B4: Kết nối Store với ứng dụng

Sử dụng Provider từ react-redux cung cấp store cho toàn bộ ứng dụng.

Typescript
import React from "react";
import { Provider } from "react-redux";
import { store } from "./store";
import TodoApp from "./components/TodoApp";

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
};

export default App;

Bước 5: Truy cập state từ Redux store

  • Trong component, bạn sử dụng useSelector và useDispatch.
Typescript
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store";
import { addTodo } from "../actions/todoActions";
import { Todo } from "../types/todo";

const TodoApp: React.FC = () => {
  const [newTodo, setNewTodo] = useState<string>("");
  const todos = useSelector((state: RootState) => state.todos);
  const dispatch = useDispatch();

  const handleAddTodo = () => {
    if (newTodo) {
      const todo: Todo = { id: Date.now(), text: newTodo };
      dispatch(addTodo(todo));
      setNewTodo("");
    }
  };

  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button onClick={handleAddTodo}>Add Todo</button>
    </div>
  );
};

export default TodoApp;

4. Giới thiệu Recoil

4.1 Recoil là gì?

Recoil là một thư viện quản lý state, giải quyết các vấn đề về quản lý state phức tạp mà không cần cấu hình nhiều như Redux. Một trong những tính năng mạnh mẽ của Recoil là khả năng quản lý state toàn cục mà không làm ảnh hưởng đến sự rõ ràng và cấu trúc của component tree.

recoil là gì

4.2 Khi nào nên sử dụng Recoil?

  • Bạn cần quản lý nhiều state độc lập và cần sự linh hoạt cao trong việc share dữ liệu giữa components.
  • Bạn muốn giải pháp quản lý state đơn giản mà không cồng kềnh như Redux.
  • Bạn cần công cụ hỗ trợ performance tốt, chỉ re-render những component cần thiết khi state thay đổi.

4.3 Ưu, nhược điểm của Recoil

Ưu điểm:

  • Dễ tích hợp với React mà không cần phải cấu hình phức tạp.
  • Chỉ re-render component liên quan khi state thay đổi, giúp tối ưu hiệu suất.
  • Sử dụng selector để tính toán derived state (trạng thái không được lưu trữ trực tiếp) từ các atom một cách tối ưu.

Nhược điểm:

  • Thư viện còn tương đối mới, nên có thể gặp một số hạn chế khi sử dụng trong các dự án lớn.

4.4 Hướng dẫn sử dụng Recoil trong dự án React

B1: Cài đặt thư viện

Bash
npm install recoil

B2: Tạo Atom và Selector

Atom là đơn vị cơ bản của state trong Recoil, còn Selector là nơi để tạo ra derived state từ Atom hoặc từ các Selector khác.

Typescript
import { atom, selector } from 'recoil';

export const countState = atom<number>({
  key: 'countState', // Mỗi atom cần key duy nhất
  default: 0,
});

export const doubledCountState = selector<number>({
  key: 'doubledCountState',
  get: ({ get }) => {
    const count = get(countState);
    return count * 2;
  },
});

B3: Sử dụng RecoilRoot để bọc ứng dụng

Typescript
import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      {/* components của ứng dụng */}
    </RecoilRoot>
  );
}

export default App;

B4: Truy cập Atom hoặc Selector trong component

Sử dụng hook useRecoilState, useRecoilValue, hoặc useSetRecoilState để truy cập state.

Typescript
import { useRecoilState, useRecoilValue } from 'recoil';
import { countState, doubledCountState } from './state';

function Counter() {
  const [count, setCount] = useRecoilState(countState);
  const doubledCount = useRecoilValue(doubledCountState);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled Count: {doubledCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

5. Giới thiệu React Query

5.1 React Query là gì?

React Query là một thư viện quản lý state hướng dữ liệu, giúp quản lý quá trình gọi API, caching, và sync dữ liệu, tối ưu hóa việc lấy dữ liệu từ server và quản lý hiệu quả các trạng thái không đồng bộ trong ứng dụng React.

react query la gì

5.2 Khi nào nên sử dụng React Query?

  • Khi ứng dụng có nhiều yêu cầu fetch dữ liệu từ server.
  • Khi bạn cần caching dữ liệu tự động và cơ chế refetch mà không cần tự viết logic này.
  • Khi bạn muốn dễ dàng quản lý các trạng thái loading, error, và success của các request bất đồng bộ.

5.3 Ưu, nhược điểm của React Query

Ưu điểm:

  • Tự động caching, re-fetching, và syncing dữ liệu khi cần.
  • Quản lý trạng thái liên quan đến API dễ dàng, giảm bớt lượng code phải viết để xử lý fetch và cập nhật dữ liệu.
  • Hỗ trợ paginations, infinite scrolling, và nhiều tính năng liên quan đến dữ liệu không đồng bộ.

Nhược điểm:

  • Không phải là giải pháp quản lý state toàn diện, chỉ tập trung vào dữ liệu bất đồng bộ.
  • Đòi hỏi kiến thức tốt làm việc với API.

5.4 Hướng dẫn sử dụng React Query cho dự án React

B1: Cài đặt React Query

Bash
npm install react-query
npm install @tanstack/react-query-devtools

B2: Thiết lập QueryClient:

Sử dụng QueryClientProvider để cung cấp React Query cho toàn bộ ứng dụng.

Typescript
import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Các components */}
    </QueryClientProvider>
  );
}

export default App;

B3: Sử dụng hook useQuery để fetch dữ liệu:

Typescript
import { useQuery } from 'react-query';
import axios from 'axios';

interface Post {
  id: number;
  title: string;
}

function fetchPosts(): Promise<Post[]> {
  return axios.get('/api/posts').then(res => res.data);
}

function Posts() {
  const { data, error, isLoading } = useQuery<Post[]>('posts', fetchPosts);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data?.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

6. Giới thiệu Zustand

6.1 Zustand là gì?

Zustand là thư viện quản lý state nhẹ dành cho React, nổi bật bởi sự đơn giản và dễ sử dụng. Khác với Redux, Zustand không yêu cầu quá nhiều cấu hình, giúp giảm thiểu sự phức tạp trong việc quản lý state.

zustand là gì

6.2 Khi nào nên sử dụng Zustand?

  • Khi cần quản lý state toàn cục nhưng không muốn cấu hình phức tạp.
  • Khi bạn muốn sử dụng thư viện nhỏ gọn, không gây ảnh hưởng đến hiệu suất.

6.3 Ưu, nhược điểm của Zustand

Ưu điểm

  • Rất nhẹ và đơn giản, không yêu cầu nhiều cấu hình.
  • Dễ hiểu, dễ sử dụng cho các ứng dụng vừa và nhỏ.
  • Không re-render không cần thiết, giúp tối ưu hiệu suất.

Nhược điểm:

  • Không phù hợp cho các ứng dụng lớn với nhiều state phức tạp.

6.4 Hướng dẫn sử dụng Zustand cho dự án React

B1: Cài đặt Zustand

Bash
npm install zustand

B2: Tạo store

Typescript
import create from 'zustand';

interface CounterState {
  count: number;
  increment: () => void;
}

const useStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

B3: Sử dụng store trong component

Typescript
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

7. Giới thiệu XState

7.1 XState là gì?

XState là thư viện quản lý state dựa trên state machine và statechart, cung cấp cách tiếp cận có hệ thống để quản lý các trạng thái phức tạp trong ứng dụng.

xstate là gì

7.2 Khi nào nên sử dụng XState?

  • Khi ứng dụng của bạn có logic phức tạp với nhiều trạng thái chuyển đổi liên tục dựa trên các sự kiện.
  • Khi bạn muốn quản lý state một cách rõ ràng và có thể dự đoán trước.

7.3 Ưu, nhược điểm của XState

Ưu điểm

  • Quản lý các trạng thái và sự kiện một cách có hệ thống, giúp dễ dàng dự đoán và kiểm soát logic.
  • Phù hợp cho những ứng dụng yêu cầu nhiều trạng thái và các luồng chuyển đổi phức tạp.

Nhược điểm:

  • Cần có kiến thức về state machines, có thể phức tạp đối với những dự án nhỏ, đơn giản.
  • Mất nhiều thời gian và công sức để cấu hình và xây dựng state machine.

7.4 Hướng dẫn sử dụng XState cho dự án React

B1: Cài đặt XState

Bash
npm install xstate @xstate/react

B2: Tạo machine

Typescript
import { createMachine, assign } from 'xstate';

interface ToggleContext {
  count: number;
}

type ToggleEvent = { type: 'TOGGLE' };

const toggleMachine = createMachine<ToggleContext, ToggleEvent>({
  id: 'toggle',
  initial: 'inactive',
  context: {
    count: 0,
  },
  states: {
    inactive: {
      on: {
        TOGGLE: {
          target: 'active',
          actions: assign({
            count: (context) => context.count + 1,
          }),
        },
      },
    },
    active: {
      on: { TOGGLE: 'inactive' },
    },
  },
});

B3: Sử dụng machine trong component

Sử dụng hook useMachine để tích hợp XState vào component.

Typescript
import { useMachine } from '@xstate/react';
import { toggleMachine } from './toggleMachine';

function Toggle() {
  const [state, send] = useMachine(toggleMachine);

  return (
    <div>
      <p>{state.matches('inactive') ? 'Inactive' : 'Active'}</p>
      <p>Count: {state.context.count}</p>
      <button onClick={() => send('TOGGLE')}>Toggle</button>
    </div>
  );
}

8. Kết luận

Việc lựa chọn giải pháp quản lý state trong React phụ thuộc nhiều vào quy mô và độ phức tạp của ứng dụng.

Việc hiểu rõ đặc điểm của từng thư viện sẽ giúp bạn chọn được giải pháp phù hợp nhất cho dự án của mình, từ đó tối ưu hóa hiệu suất và giảm thiểu công sức bảo trì mã nguồn.

Các bài viết liên quan:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab