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.
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:
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:
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.
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.
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:
- Store: chứa toàn bộ trạng thái của ứng dụng.
- Action: sự kiện yêu cầu thay đổi state.
- 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.
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:
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
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:
export interface Todo {
id: number;
text: string;
}
export interface AddTodoAction {
type: "ADD_TODO";
payload: Todo;
}
export type TodoActionTypes = AddTodoAction;
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:
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.
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.
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.
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
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.
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
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.
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.
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
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.
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:
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.
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
npm install zustand
B2: Tạo store
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
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.
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
npm install xstate @xstate/react
B2: Tạo machine
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.
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
React Toastify là gì? Hướng dẫn sử dụng Toast Notification với React Toastify
Nov 21, 2024 • 7 min read
Hướng dẫn sử dụng Zustand trong NextJS
Nov 21, 2024 • 8 min read
Lazy Loading: Kỹ thuật Tối ưu Hiệu suất Website
Nov 17, 2024 • 14 min read
Hướng dẫn sử dụng Redux Toolkit và Redux Saga trong dự án React
Nov 15, 2024 • 10 min read
WebGL là gì? Hướng dẫn tạo đồ họa đơn giản với WebGL
Nov 13, 2024 • 7 min read
Test-Driven Development (TDD) là gì? Hướng dẫn thực hành TDD
Nov 13, 2024 • 6 min read