Tiếng Việt

Khám phá các mẫu nâng cao cho React Context API, bao gồm component phức hợp, context động, và các kỹ thuật tối ưu hiệu suất để quản lý trạng thái phức tạp.

Các Mẫu Nâng Cao của React Context API để Quản Lý Trạng Thái

React Context API cung cấp một cơ chế mạnh mẽ để chia sẻ trạng thái trong toàn bộ ứng dụng của bạn mà không cần "prop drilling" (truyền props qua nhiều cấp). Mặc dù cách sử dụng cơ bản khá đơn giản, việc tận dụng toàn bộ tiềm năng của nó đòi hỏi sự hiểu biết về các mẫu nâng cao có thể xử lý các kịch bản quản lý trạng thái phức tạp. Bài viết này khám phá một số mẫu đó, cung cấp các ví dụ thực tế và thông tin chi tiết hữu ích để nâng cao kỹ năng phát triển React của bạn.

Hiểu Rõ Hạn Chế của Context API Cơ Bản

Trước khi đi sâu vào các mẫu nâng cao, điều quan trọng là phải thừa nhận những hạn chế của Context API cơ bản. Mặc dù phù hợp với trạng thái đơn giản, có thể truy cập toàn cục, nó có thể trở nên cồng kềnh và không hiệu quả đối với các ứng dụng phức tạp có trạng thái thay đổi thường xuyên. Mọi component sử dụng một context đều sẽ render lại mỗi khi giá trị của context thay đổi, ngay cả khi component đó không phụ thuộc vào phần trạng thái cụ thể đã được cập nhật. Điều này có thể dẫn đến các vấn đề về hiệu suất.

Mẫu 1: Component Phức Hợp (Compound Components) với Context

Mẫu Component Phức Hợp cải tiến Context API bằng cách tạo ra một bộ các component liên quan chia sẻ trạng thái và logic một cách ngầm định thông qua một context. Mẫu này thúc đẩy khả năng tái sử dụng và đơn giản hóa API cho người dùng. Điều này cho phép logic phức tạp được đóng gói với cách triển khai đơn giản.

Ví dụ: Component Tab

Hãy minh họa điều này với một component Tab. Thay vì truyền props xuống qua nhiều lớp, các component Tab giao tiếp ngầm định thông qua một context được chia sẻ.

// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabContext = createContext(undefined);

interface TabProviderProps {
  children: ReactNode;
  defaultTab: string;
}

export const TabProvider: React.FC = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const value: TabContextType = {
    activeTab,
    setActiveTab,
  };

  return {children};
};

export const useTabContext = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTabContext must be used within a TabProvider');
  }
  return context;
};

// TabList.js
import React, { ReactNode } from 'react';

interface TabListProps {
  children: ReactNode;
}

export const TabList: React.FC = ({ children }) => {
  return 
{children}
; }; // Tab.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabProps { label: string; children: ReactNode; } export const Tab: React.FC = ({ label, children }) => { const { activeTab, setActiveTab } = useTabContext(); const isActive = activeTab === label; const handleClick = () => { setActiveTab(label); }; return ( ); }; // TabPanel.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabPanelProps { label: string; children: ReactNode; } export const TabPanel: React.FC = ({ label, children }) => { const { activeTab } = useTabContext(); const isActive = activeTab === label; return ( ); };
// Cách sử dụng
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Nội dung cho Tab 1
      Nội dung cho Tab 2
      Nội dung cho Tab 3
    
  );
}

export default App;

Lợi ích:

Mẫu 2: Context Động (Dynamic Contexts)

Trong một số trường hợp, bạn có thể cần các giá trị context khác nhau dựa trên vị trí của component trong cây component hoặc các yếu tố động khác. Context động cho phép bạn tạo và cung cấp các giá trị context thay đổi dựa trên các điều kiện cụ thể.

Ví dụ: Tạo Theme với Context Động

Hãy xem xét một hệ thống theme nơi bạn muốn cung cấp các theme khác nhau dựa trên sở thích của người dùng hoặc phần ứng dụng họ đang ở. Chúng ta có thể tạo một ví dụ đơn giản với theme sáng và tối.

// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = () => {
    setIsDarkTheme(!isDarkTheme);
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme,
  };

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};
// Cách sử dụng
import { useTheme, ThemeProvider } from './ThemeContext';

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

  return (
    

Đây là một component có theme.

); } function App() { return ( ); } export default App;

Trong ví dụ này, ThemeProvider xác định theme một cách linh động dựa trên trạng thái isDarkTheme. Các component sử dụng hook useTheme sẽ tự động render lại khi theme thay đổi.

Mẫu 3: Kết hợp Context với useReducer cho Trạng Thái Phức Tạp

Để quản lý logic trạng thái phức tạp, việc kết hợp Context API với useReducer là một cách tiếp cận tuyệt vời. useReducer cung cấp một cách có cấu trúc để cập nhật trạng thái dựa trên các action, và Context API cho phép bạn chia sẻ trạng thái này và hàm dispatch trong toàn bộ ứng dụng của mình.

Ví dụ: Danh sách công việc đơn giản (Todo List)

// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

interface TodoContextType {
  state: TodoState;
  dispatch: React.Dispatch;
}

const initialState: TodoState = {
  todos: [],
};

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};

const TodoContext = createContext(undefined);

interface TodoProviderProps {
  children: ReactNode;
}

export const TodoProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value: TodoContextType = {
    state,
    dispatch,
  };

  return {children};
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
};
// Cách sử dụng
import { useTodo, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function AddTodo() { const { dispatch } = useTodo(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Mẫu này tập trung logic quản lý trạng thái vào reducer, giúp việc suy luận và kiểm thử trở nên dễ dàng hơn. Các component có thể dispatch các action để cập nhật trạng thái mà không cần phải quản lý trạng thái trực tiếp.

Mẫu 4: Tối Ưu Hóa Cập Nhật Context với 'useMemo' và 'useCallback'

Như đã đề cập trước đó, một vấn đề hiệu suất quan trọng với Context API là các lần render lại không cần thiết. Việc sử dụng useMemouseCallback có thể ngăn chặn các lần render lại này bằng cách đảm bảo rằng chỉ những phần cần thiết của giá trị context được cập nhật, và các tham chiếu hàm vẫn ổn định.

Ví dụ: Tối ưu hóa Theme Context

// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = useCallback(() => {
    setIsDarkTheme(!isDarkTheme);
  }, [isDarkTheme]);

  const value: ThemeContextType = useMemo(() => ({
    theme,
    toggleTheme,
  }), [theme, toggleTheme]);

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};

Giải thích:

Nếu không có useCallback, hàm toggleTheme sẽ được tạo lại mỗi khi ThemeProvider render, làm cho value thay đổi và kích hoạt các lần render lại trong bất kỳ component nào sử dụng nó, ngay cả khi chính theme không thay đổi. useMemo đảm bảo một value mới chỉ được tạo ra khi các phụ thuộc của nó (theme hoặc toggleTheme) thay đổi.

Mẫu 5: Bộ chọn Context (Context Selectors)

Bộ chọn context cho phép các component chỉ đăng ký nhận các phần cụ thể của giá trị context. Điều này ngăn chặn các lần render lại không cần thiết khi các phần khác của context thay đổi. Các thư viện như `use-context-selector` hoặc các triển khai tùy chỉnh có thể được sử dụng để đạt được điều này.

Ví dụ Sử dụng Bộ chọn Context Tùy chỉnh

// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';

function useCustomContextSelector(
  context: React.Context,
  selector: (value: T) => S
): S {
  const value = useContext(context);
  const [selected, setSelected] = useState(() => selector(value));
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useEffect(() => {
    let didUnmount = false;
    let lastSelected = selected;

    const subscription = () => {
      if (didUnmount) {
        return;
      }
      const nextSelected = latestSelector.current(value);
      if (!Object.is(lastSelected, nextSelected)) {
        lastSelected = nextSelected;
        setSelected(nextSelected);
      }
    };

    // Bạn thường sẽ đăng ký nhận thay đổi context ở đây. Vì đây là một ví dụ đơn giản,
    // chúng ta sẽ chỉ gọi subscription ngay lập tức để khởi tạo.
    subscription();

    return () => {
      didUnmount = true;
      // Hủy đăng ký nhận thay đổi context ở đây, nếu có.
    };
  }, [value]); // Chạy lại effect mỗi khi giá trị context thay đổi

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Đơn giản hóa cho ngắn gọn)
import React, { createContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

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

const ThemeContext = createContext(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme: Theme;
}

export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
  const [theme, setTheme] = useState(initialTheme);

  const value: ThemeContextType = {
    theme,
    setTheme
  };

  return {children};
};

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

export default ThemeContext;
// Cách sử dụng
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Nền
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Màu chữ
; } function App() { const { theme, setTheme } = useThemeContext(); const toggleTheme = () => { setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' }); }; return ( ); } export default App;

Trong ví dụ này, BackgroundComponent chỉ render lại khi thuộc tính background của theme thay đổi, và ColorComponent chỉ render lại khi thuộc tính color thay đổi. Điều này tránh được các lần render lại không cần thiết khi toàn bộ giá trị context thay đổi.

Mẫu 6: Tách Biệt Actions khỏi State

Đối với các ứng dụng lớn hơn, hãy xem xét việc tách giá trị context thành hai context riêng biệt: một cho trạng thái (state) và một cho các hành động (actions - các hàm dispatch). Điều này có thể cải thiện việc tổ chức mã và khả năng kiểm thử.

Ví dụ: Todo List với Context State và Action riêng biệt

// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

const initialState: TodoState = {
  todos: [],
};

const TodoStateContext = createContext(initialState);

interface TodoStateProviderProps {
  children: ReactNode;
}

export const TodoStateProvider: React.FC = ({ children }) => {
  const [state] = useReducer(todoReducer, initialState);

  return {children};
};

export const useTodoState = () => {
  return useContext(TodoStateContext);
};

// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

const TodoActionContext = createContext | undefined>(undefined);

interface TodoActionProviderProps {
    children: ReactNode;
}

export const TodoActionProvider: React.FC = ({children}) => {
    const [, dispatch] = useReducer(todoReducer, initialState);

    return {children};
};


export const useTodoDispatch = () => {
  const dispatch = useContext(TodoActionContext);
  if (!dispatch) {
    throw new Error('useTodoDispatch must be used within a TodoActionProvider');
  }
  return dispatch;
};

// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};
// Cách sử dụng
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';

function TodoList() {
  const state = useTodoState();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function TodoActions({ todo }) { const dispatch = useTodoDispatch(); return ( <> ); } function AddTodo() { const dispatch = useTodoDispatch(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Sự tách biệt này cho phép các component chỉ đăng ký nhận context mà chúng cần, giảm thiểu các lần render lại không cần thiết. Nó cũng giúp việc kiểm thử đơn vị (unit test) reducer và mỗi component một cách độc lập trở nên dễ dàng hơn. Ngoài ra, thứ tự bọc các provider cũng quan trọng. ActionProvider phải bọc StateProvider.

Các Thực Tiễn Tốt Nhất và Những Điều Cần Cân Nhắc

Kết luận

React Context API là một công cụ đa năng để quản lý trạng thái. Bằng cách hiểu và áp dụng các mẫu nâng cao này, bạn có thể quản lý hiệu quả trạng thái phức tạp, tối ưu hóa hiệu suất và xây dựng các ứng dụng React dễ bảo trì và mở rộng hơn. Hãy nhớ chọn đúng mẫu cho nhu cầu cụ thể của bạn và cân nhắc cẩn thận các tác động về hiệu suất của việc sử dụng context.

Khi React phát triển, các thực tiễn tốt nhất xung quanh Context API cũng sẽ thay đổi. Việc cập nhật thông tin về các kỹ thuật và thư viện mới sẽ đảm bảo bạn được trang bị để xử lý các thách thức quản lý trạng thái của phát triển web hiện đại. Hãy cân nhắc khám phá các mẫu mới nổi như sử dụng context với signals để có khả năng phản ứng (reactivity) chi tiết hơn nữa.