Thay thế Redux: Quản lý trạng thái nâng cao với React Context và useReducer

Table of Contents
Trong nhiều năm, Redux là ông hoàng không thể tranh cãi trong việc quản lý trạng thái của React. Nó cung cấp một state container dễ đoán, một hệ sinh thái middleware mạnh mẽ và một trình gỡ lỗi "du hành thời gian" khiến các nhà phát triển phải mê mẩn. Tuy nhiên, khi React phát triển, sự rườm rà và phức tạp của Redux bắt đầu trở thành gánh nặng hơn là một lợi ích cho nhiều ứng dụng.
Với sự ra đời của Hooks, đặc biệt là useContext và useReducer, React đã cung cấp các primitive tích hợp sẵn có thể thay thế Redux cho một phần đáng kể các trường hợp sử dụng. Mặc dù nhiều hướng dẫn—chẳng hạn như của Robin Wieruch—đã làm rất tốt việc giới thiệu các khái niệm này, nhưng chúng thường không giải quyết được các nút thắt cổ chai về hiệu suất quan trọng, các thách thức về kiến trúc và các yêu cầu về middleware phát sinh ở quy mô lớn.
Trong hướng dẫn toàn diện, mang tính kỹ thuật cao này, chúng ta sẽ vượt ra ngoài những điều cơ bản. Chúng ta sẽ xây dựng một giải pháp quản lý trạng thái được tối ưu hóa, có khả năng mở rộng bằng cách sử dụng Context và useReducer, tập trung vào hiệu suất rendering, phân tách trạng thái (state slicing), các mẫu middleware tùy chỉnh và cách cạnh tranh với Redux về cả Trải nghiệm nhà phát triển (DX) và hiệu quả runtime.
Vấn đề với Context và useReducer đơn giản
Cách tiếp cận phổ biến nhất để thay thế Redux là bọc toàn bộ ứng dụng (hoặc một phần lớn) trong một Context Provider duy nhất, truyền xuống một đối tượng trạng thái và một hàm dispatch.
// Naive implementation
import { createContext, useReducer } from 'react';
const AppContext = createContext();
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
Mặc dù điều này hoạt động hoàn hảo cho các ứng dụng nhỏ, nhưng nó lại gây ra một lỗi hiệu suất lớn ở quy mô lớn: Context Propagation.
Bất cứ khi nào state thay đổi, mọi component sử dụng AppContext thông qua useContext(AppContext) sẽ re-render. Điều này xảy ra bất kể component đó có thực sự sử dụng phần trạng thái cụ thể đã được cập nhật hay không. Redux tránh được cạm bẫy này một cách tự nhiên thông qua hook useSelector của nó, hook này chỉ đăng ký các phần cụ thể của store và chủ động bỏ qua việc rendering nếu phần được chọn không thay đổi.
Để thực sự thay thế Redux trong một ứng dụng hiện đại, hiệu suất cao, chúng ta phải giải quyết vấn đề re-render.
Tối ưu hóa kiến trúc 1: Tách Context của State và Dispatch
Bước đầu tiên trong hành trình tối ưu hóa của chúng ta là tách các hàm state và dispatch thành các context hoàn toàn riêng biệt. Hàm dispatch được trả về bởi useReducer là ổn định—tham chiếu bộ nhớ của nó không bao giờ thay đổi qua các lần re-render. Bằng cách cô lập nó trong provider riêng của nó, các component chỉ cần kích hoạt các hành động (như nút "Submit" hoặc "Toggle") có thể sử dụng context dispatch mà không cần re-render khi trạng thái thay đổi.
import React, { createContext, useReducer } from 'react';
const StateContext = createContext(null);
const DispatchContext = createContext(null);
export const AppStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
};
Bây giờ, một ThemeToggleButton có thể sử dụng DispatchContext để kích hoạt một hành động TOGGLE_THEME. Vì tham chiếu đến dispatch không bao giờ thay đổi, component này sẽ không bao giờ re-render một cách vô ích khi trạng thái toàn cục cập nhật.
Tối ưu hóa kiến trúc 2: Phân tách theo miền (Domain-Driven Slicing)
Ngay cả khi dispatch đã được tách riêng, StateContext vẫn giữ toàn bộ trạng thái toàn cục. Nếu người dùng cập nhật ảnh đại diện của họ, một SidebarMenu được lồng sâu sử dụng StateContext chỉ để đọc vai trò của người dùng vẫn sẽ re-render.
Thay vì một store nguyên khối, chúng ta nên phân tách trạng thái của mình theo miền. React cho phép chúng ta kết hợp nhiều provider độc lập một cách thanh lịch.
export const GlobalProvider = ({ children }) => (
<AuthProvider>
<ThemeProvider>
<ShoppingCartProvider>
{children}
</ShoppingCartProvider>
</ThemeProvider>
</AuthProvider>
);
Bằng cách cô lập trạng thái, chúng ta đảm bảo rằng việc cập nhật giỏ hàng chỉ kích hoạt re-render trong các component sử dụng rõ ràng ShoppingCartContext. Điều này phản ánh chặt chẽ các slice của Redux, nhưng tận dụng kiến trúc cây gốc của React.
Tối ưu hóa kiến trúc 3: Context Selector thông qua Memoization
Ngay cả với việc phân tách theo miền, một miền phức tạp (ví dụ: UserContext) có thể chứa các trường như permissions, preferences và activityLog. Nếu một component chỉ cần permissions, nó không nên re-render khi activityLog thay đổi.
API Context của React không hỗ trợ selector một cách tự nhiên (mặc dù các đề xuất useContextSelector đang được tiến hành). Tuy nhiên, chúng ta có thể đạt được điều này bằng cách sử dụng Higher-Order Components (HOCs) và React.memo để mô phỏng hành vi rendering chính xác của Redux.
import React, { useContext, memo } from 'react';
// The presentation component is memoized
const UserProfile = memo(({ username, role }) => {
console.log("UserProfile rendered!");
return (
<div className="profile-card">
<h1>{username}</h1>
<span>{role}</span>
</div>
);
});
// The wrapper component consumes context
const UserProfileContainer = () => {
const { username, role, lastLogin } = useContext(UserContext);
// lastLogin updates frequently, but UserProfile only receives username and role.
// Because UserProfile is wrapped in memo, it will bail out of the reconciliation phase!
return <UserProfile username={username} role={role} />;
};
Mẫu này hoạt động như một pseudo-selector. UserProfileContainer re-render mỗi khi UserContext thay đổi, điều này cực kỳ nhanh. Tuy nhiên, vì nó trả về một <UserProfile /> đã được memoize, React dừng giai đoạn render trước khi nó đến các hoạt động diffing Virtual DOM tốn kém.
Tái tạo Middleware: useReducer nâng cao
Một trong những điểm mạnh lớn nhất của Redux là hệ sinh thái middleware của nó—cho phép bạn chặn các hành động để ghi log, phân tích hoặc các hoạt động bất đồng bộ. Chúng ta có thể tái tạo điều này bằng cách bọc useReducer gốc của React.
Hãy xây dựng một custom hook để thêm khả năng ghi log và thunk vào useReducer:
import { useReducer, useCallback, useRef } from 'react';
const useEnhancedReducer = (reducer, initialState, middlewares = []) => {
const [state, dispatch] = useReducer(reducer, initialState);
const stateRef = useRef(state);
// Keep state ref updated for middleware access
stateRef.current = state;
const enhancedDispatch = useCallback(
(action) => {
// Allow thunks (functions as actions)
if (typeof action === 'function') {
return action(enhancedDispatch, () => stateRef.current);
}
// Run pre-dispatch middlewares
middlewares.forEach((mw) => mw(action, stateRef.current));
// Native dispatch
dispatch(action);
},
[middlewares]
);
return [state, enhancedDispatch];
};
Với hook nâng cao này, bạn có thể truyền các middleware tùy chỉnh giống hệt như Redux:
const loggerMiddleware = (action, state) => {
console.group(`Action: ${action.type}`);
console.log('Previous State:', state);
console.log('Payload:', action.payload);
console.groupEnd();
};
const [state, dispatch] = useEnhancedReducer(
rootReducer,
initialState,
[loggerMiddleware]
);
Quản lý logic bất đồng bộ một cách thanh lịch
Redux Thunk và Redux Saga nổi tiếng được sử dụng để xử lý các side effect. Làm thế nào để chúng ta tái tạo điều này một cách thanh lịch với Context, giả sử chúng ta không muốn xây dựng một runtime middleware tùy chỉnh?
Câu trả lời nằm ở việc trừu tượng hóa các hoạt động bất đồng bộ thành các custom hook bọc hàm dispatch của chúng ta.
export const useAuthActions = () => {
const dispatch = useContext(DispatchContext);
const loginUser = async (credentials) => {
dispatch({ type: 'LOGIN_REQUEST' });
try {
const response = await api.login(credentials);
dispatch({ type: 'LOGIN_SUCCESS', payload: response.user });
} catch (error) {
dispatch({ type: 'LOGIN_FAILURE', payload: error.message });
}
};
return { loginUser };
};
Mẫu này đóng gói logic nghiệp vụ tách biệt khỏi các component UI. Lớp trình bày của bạn chỉ cần gọi loginUser(credentials), giữ cho component hoàn toàn không phụ thuộc vào việc triển khai API. Nó cung cấp chính xác những lợi ích tách rời tương tự như Redux Thunks, nhưng với khả năng suy luận TypeScript tốt hơn và không cần thiết lập middleware.
Tính bất biến và Immer
Một điểm khó khăn phổ biến với useReducer là cập nhật các đối tượng được lồng sâu. Redux Toolkit giải quyết vấn đề này bằng cách tích hợp Immer ngay lập tức. Bạn có thể dễ dàng đạt được cùng một DevX bằng cách bọc reducer của mình bằng produce của Immer.
import { produce } from 'immer';
const userReducer = produce((draft, action) => {
switch (action.type) {
case 'UPDATE_PROFILE':
// Direct mutation! Immer handles the immutability under the hood.
draft.profile.address.city = action.payload;
break;
}
});
Việc bổ sung duy nhất này đã thu hẹp một trong những khoảng cách lớn nhất về tính tiện dụng giữa trạng thái React gốc và Redux Toolkit.
Kết luận
Thay thế Redux bằng React Context và useReducer là một lựa chọn kiến trúc mạnh mẽ, nhưng nó đòi hỏi sự tôn trọng. Các triển khai đơn giản chắc chắn sẽ dẫn đến giao diện chậm chạp và các "render storm" khi ứng dụng phát triển.
Bằng cách tách các context state và dispatch của bạn, cô lập trạng thái vào các provider dành riêng cho miền, sử dụng các kỹ thuật memoization nâng cao cho hành vi giống selector và áp dụng các custom hook cho middleware và logic bất đồng bộ, bạn có thể xây dựng một kiến trúc quản lý trạng thái có hiệu suất tương đương Redux.
Quan trọng hơn, nó chấp nhận các mẫu React thông thường, giữ cho cây dependency của bạn nhẹ nhàng và làm cho việc phát triển frontend hiện đại trở nên thú vị. React đã cung cấp cho chúng ta các primitive; việc của chúng ta là kiến trúc chúng một cách thanh lịch.
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Quản lý trạng thái trong React 2026: Vượt xa Redux
Hướng dẫn toàn diện về quản lý trạng thái React năm 2026: so sánh React 19 actions, trạng thái máy chủ TanStack Query, Zustand, Jotai và Signals.
Read more
Nắm vững SVG trong React 19: Hiệu suất, currentColor động & Tối ưu hóa Bundle
Ngừng gửi 800kB icon thừa thãi. Nắm vững SVG trong React 19 với chủ đề currentColor động, SVG sprite sheets, giao diện forwardRef và không có overhead runtime.
Read more
Sự thay đổi mô hình của React Server Components
Khám phá cách React Server Components (RSC) thay đổi cơ bản cách chúng ta xây dựng ứng dụng React, mang lại kích thước bundle nhỏ hơn, đơn giản hóa việc tìm nạp dữ liệu và cải thiện hiệu suất.
Read more