[React Basics] React Context: Khái niệm & Cách sử dụng hiệu quả nhất

Nếu bạn từng làm việc với React, chắc hẳn đã có lúc bạn phải "còng lưng" truyền một prop qua hàng chục cấp component, dù cho các component ở giữa chẳng hề cần đến nó. Trải nghiệm đó, mà cộng đồng lập trình viên vẫn hay gọi một cách hài hước là "prop drilling" (khoan props), thực sự là một cơn ác mộng. Code trở nên khó đọc, khó bảo trì và dễ phát sinh lỗi.

Nhưng đừng lo, React đã mang tới cho chúng ta một "cứu tinh": React Context.

React Context: Khái niệm & Cách sử dụng hiệu quả nhất

Trong bài viết này, chúng ta sẽ cùng nhau "mổ xẻ" React Context một cách toàn diện: nó là gì, tại sao nó tồn tại, cách sử dụng hiệu quả và khi nào thì bạn nên (và không nên) dùng nó. Hãy sẵn sàng để nói lời tạm biệt với "prop drilling" nhé!

1. React Context là gì? Một cách hiểu đơn giản nhất 🎯

Hãy tưởng tượng cây component của bạn như một tòa nhà chung cư. Prop drilling giống như việc bạn ở tầng 20 và muốn gửi một món đồ cho người ở tầng 1, nhưng lại phải nhờ lần lượt từng người ở tầng 19, 18, 17,... chuyền tay nhau món đồ đó. Rất phiền phức!

React Context tạo ra một "hệ thống thang máy" cho dữ liệu. Bạn chỉ cần đặt món đồ (dữ liệu) vào thang máy ở tầng 20 (Provider) và bất kỳ ai ở tầng nào (Consumer) có chìa khóa đều có thể lấy được món đồ đó mà không cần thông qua các tầng trung gian.

Chuyển từ Prop Drilling sang sử dụng Context API

Định nghĩa chính thức: React Context cung cấp một cách để truyền dữ liệu qua cây component mà không cần phải truyền props xuống theo cách thủ công ở mọi cấp.

Nói cách khác, nó tạo ra một "trạng thái toàn cục" (global state) cho một nhóm các component, cho phép chúng chia sẻ dữ liệu một cách dễ dàng.

2. Các mảnh ghép của React Context 🧩

Để sử dụng Context, bạn cần làm quen với 3 khái niệm chính:

  1. React.createContext():

    • Đây là hàm bạn dùng để "khởi tạo" một Context mới.
    • Hàm này trả về một đối tượng Context, bao gồm hai thành phần quan trọng là ProviderConsumer.
    • Bạn có thể truyền vào một giá trị mặc định (defaultValue), giá trị này sẽ được sử dụng khi một component không tìm thấy Provider nào ở phía trên nó trong cây component.
    // theme-context.js
    import React from 'react'
    
    // Tạo một Context với giá trị mặc định là 'light'
    const ThemeContext = React.createContext('light')
    
    export default ThemeContext
    
  2. Context.Provider:

    • Đây là "trạm phát sóng". Bất kỳ component nào được bọc trong Provider và các component con cháu của nó đều có thể "nghe" được dữ liệu mà Provider phát ra.
    • Provider nhận một prop quan trọng là value. Đây chính là dữ liệu bạn muốn chia sẻ.
    // App.js
    import ThemeContext from './theme-context'
    import Toolbar from './Toolbar'
    
    function App() {
      const theme = 'dark' // Dữ liệu chúng ta muốn chia sẻ
    
      return (
        <ThemeContext.Provider value={theme}>
          <Toolbar />
        </ThemeContext.Provider>
      )
    }
    
  3. useContext(Context):

    • Đây là cách hiện đại và phổ biến nhất để "tiêu thụ" hay "lắng nghe" dữ liệu từ Provider.
    • Bạn chỉ cần gọi hook useContext và truyền vào đối tượng Context mà bạn đã tạo. Hook này sẽ trả về giá trị value gần nhất từ Provider phía trên nó.
    // ThemedButton.js
    import React, { useContext } from 'react'
    import ThemeContext from './theme-context'
    
    function ThemedButton() {
      const theme = useContext(ThemeContext) // 'theme' sẽ nhận giá trị 'dark'
    
      return (
        <button
          style={{
            background: theme === 'dark' ? '#333' : '#FFF',
            color: theme === 'dark' ? '#FFF' : '#333',
          }}
        >
          I am a {theme} button
        </button>
      )
    }
    

3. Ví dụ thực tế: Chức năng chuyển đổi Light/Dark Mode 💡

Lý thuyết là vậy, hãy cùng xây dựng một ví dụ hoàn chỉnh để thấy sức mạnh của Context.

Bước 1: Tạo Context

Tạo một file để quản lý theme.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react'

// 1. Khởi tạo Context
export const ThemeContext = createContext()

// 2. Tạo Provider Component (một best practice)
export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light')

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
  }

  // 3. Truyền state và hàm thay đổi state qua prop "value"
  const value = {
    theme,
    toggleTheme,
  }

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

Bước 2: Bọc ứng dụng với Provider

Trong file App.js hoặc file gốc của ứng dụng, hãy bọc các component của bạn bằng ThemeProvider.

// src/App.js
import React from 'react'
import { ThemeProvider } from './contexts/ThemeContext'
import HomePage from './components/HomePage'

function App() {
  return (
    <ThemeProvider>
      <HomePage />
    </ThemeProvider>
  )
}

export default App

Bước 3: Sử dụng dữ liệu ở bất kỳ đâu

Bây giờ, bất kỳ component con nào bên trong HomePage cũng có thể truy cập themetoggleTheme một cách dễ dàng.

// src/components/Navbar.js
import React, { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'

const Navbar = () => {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <nav
      style={{
        background: theme === 'dark' ? '#222' : '#eee',
        padding: '1rem',
      }}
    >
      <span>My App</span>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
    </nav>
  )
}

export default Navbar

Như bạn thấy, Navbar không cần nhận bất kỳ prop nào liên quan đến theme. Nó trực tiếp lấy dữ liệu từ Context. Thật gọn gàng và hiệu quả!

4. Khi nào nên và không nên dùng React Context? 🤔

Context rất mạnh mẽ, nhưng không phải là "viên đạn bạc" cho mọi vấn đề về quản lý trạng thái.

✅ Nên dùng khi:

  1. Dữ liệu toàn cục, ít thay đổi: Rất phù hợp cho các dữ liệu như thông tin người dùng đã đăng nhập, theme (UI), ngôn ngữ của trang web.
  2. Tránh Prop Drilling: Đây là mục đích chính của nó. Khi bạn thấy mình phải truyền prop qua 2-3 cấp độ trở lên, hãy cân nhắc dùng Context.
  3. Quản lý trạng thái đơn giản: Đối với các ứng dụng nhỏ và vừa, Context kết hợp với useState hoặc useReducer là một giải pháp quản lý trạng thái tuyệt vời mà không cần thư viện bên ngoài.

❌ Không nên dùng khi:

  1. Trạng thái thay đổi với tần suất cao: Ví dụ như trạng thái của các ô input trong một form phức tạp. Mỗi khi Context value thay đổi, tất cả các component đang "tiêu thụ" Context đó sẽ bị re-render. Điều này có thể gây ra các vấn đề về hiệu năng nghiêm trọng.
  2. Thay thế hoàn toàn các thư viện quản lý trạng thái chuyên dụng (Redux, Recoil, Zustand,...): Vì React Context không có sẵn các công cụ mạnh mẽ như Redux DevTools, middleware, hay các cơ chế tối ưu hóa phức tạp. Đối với các ứng dụng lớn với luồng dữ liệu phức tạp, các thư viện này vẫn là lựa chọn tốt hơn.

5. React Context vs. Redux: Cuộc đối đầu kinh điển ⚔️

Đây là câu hỏi muôn thuở của các lập trình viên React.

React Context vs. Redux

Tiêu ChíReact ContextRedux
Mục đíchGiải quyết vấn đề "prop drilling".Quản lý trạng thái phức tạp, có thể dự đoán được.
Độ phức tạpĐơn giản, có sẵn trong React.Phức tạp hơn, cần cài đặt thư viện, nhiều khái niệm (actions, reducers, store...).
Trường hợp sử dụngDữ liệu toàn cục, ít thay đổi (theme, user auth).Ứng dụng lớn, trạng thái thay đổi thường xuyên, luồng dữ liệu phức tạp.
Hiệu năngCó thể gây re-render không cần thiết nếu không tối ưu.Tối ưu hóa cao, chỉ re-render các component bị ảnh hưởng trực tiếp.
Công cụKhông có công cụ gỡ lỗi chuyên dụng.Redux DevTools cực kỳ mạnh mẽ để theo dõi và gỡ lỗi trạng thái.

Context không phải là kẻ thù của Redux. Chúng là hai công cụ khác nhau để giải quyết các vấn đề khác nhau. Nhiều ứng dụng lớn sử dụng cả hai: Context cho dữ liệu tĩnh toàn cục và Redux cho trạng thái động phức tạp của ứng dụng.

6. Tối ưu hóa hiệu năng với React Context 🚀

Để tránh các vấn đề về hiệu năng đã đề cập, hãy ghi nhớ các kỹ thuật sau:

  1. Chia nhỏ Context: Thay vì tạo một Context "khổng lồ" chứa mọi thứ, hãy chia nó thành các Context nhỏ, chuyên biệt. Ví dụ: ThemeContext riêng, AuthContext riêng. Điều này đảm bảo rằng khi dữ liệu auth thay đổi, các component chỉ quan tâm đến theme sẽ không bị re-render.

  2. Sử dụng React.memo: Bọc các component con "tiêu thụ" Context bằng React.memo để ngăn chúng re-render nếu props của chúng không thay đổi, ngay cả khi Context cha bị re-render.

  3. Sử dụng useMemo cho giá trị value: Khi truyền một đối tượng hoặc mảng vào prop value của Provider, hãy bọc nó trong useMemo để đảm bảo đối tượng đó không được tạo lại sau mỗi lần render, tránh gây ra các re-render không cần thiết cho tất cả các consumer.

    // Tối ưu hóa Provider
    const value = useMemo(
      () => ({
        theme,
        toggleTheme,
      }),
      [theme],
    ) // Chỉ tạo lại object khi 'theme' thay đổi
    
    return (
      <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
    )
    

Kết luận: React Context là công cụ đầy tiện ích

React Context thực sự là một công cụ mạnh mẽ và tiện ích, nó được tích hợp sẵn trong React, giúp giải quyết triệt để vấn đề "prop drilling" phiền toái. Bằng cách hiểu rõ cách thức hoạt động, các trường hợp sử dụng lý tưởng và các kỹ thuật tối ưu hóa, bạn có thể viết mã React sạch hơn, dễ bảo trì hơn và có cấu trúc tốt hơn.

Hãy tự tin áp dụng React Context vào các dự án tiếp theo của bạn và tận hưởng sự tự do khi không còn phải "khoan" props nữa!

Bài viết liên quan

[React Basics] State trong React: Khái niệm, cách sử dụng và ví dụ chi tiết

Tìm hiểu khái niệm, cách khai báo và sử dụng State để quản lý dữ liệu động trong ứng dụng React của bạn. Xem ngay hướng dẫn chi tiết kèm ví dụ.

[React Basics] Props trong React: Khái niệm, cách dùng và ví dụ thực tế

Giải mã props trong ReactJS: từ khái niệm cơ bản đến cách truyền dữ liệu giữa các component một cách hiệu quả. Khám phá các ví dụ minh họa dễ hiểu để bạn áp dụng ngay.

[React Basics] React Handling Events: Những cách làm hiệu quả và tối ưu

Handling Events là một kỹ năng quan trọng trong React. Bài viết này sẽ giúp bạn hiểu rõ về cú pháp, cách truyền đối số và quản lý trạng thái khi làm việc với các sự kiện.

[React Basics] Thuộc tính key trong React: Hiểu rõ và sử dụng hiệu quả

Bạn đã thực sự hiểu về thuộc tính key trong React? Tìm hiểu vai trò, cách dùng hiệu quả và các ví dụ thực tế giúp code của bạn sạch và tối ưu hơn.