•10 min read

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

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

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.

Audio Briefing
0:00 / 0:00

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.

Advertisement

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.

Advertisement

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

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Sự thay đổi mô hình của React Server Components
react

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