Skip to content
Coding

React 初心者大補帖(6):跨元件共用的 Provider & Context

不管在使用 Vue 或是 React 開發時,常常遇到一個小困擾是:為了讓最底層的小組件拿到資料,需要一層一層傳遞 props,在 trace code 的時候就要一層一層爬回去,還有可能會在中間被篡改值,最後爬到迷路。

以購物網站為例,假設我們需要把使用者資訊、主題設定、購物車狀態傳到各種組件裡:

function App() { 
  const [user, setUser] = useState(null); 
  const [theme, setTheme] = useState('light'); 
  const [cart, setCart] = useState([]); 
 
  return ( 
    <Layout user={user} theme={theme} cart={cart} setUser={setUser} /*... 更多 props*/ > 
      {/* 其他組件 */} 
    </Layout> 
  ); 
} 
 
function Layout({ user, theme, cart, setUser, ... }) { 
  return ( 
    <div> 
      <Header user={user} theme={theme} cart={cart} /*... 又是一堆 props*/ /> 
      <Main user={user} theme={theme} /*...*/ /> 
      <Footer theme={theme} /*...*/ /> 
    </div> 
  ); 
} 
 
// 然後 Header、Main、Footer 又要繼續往下傳 QQ

這樣可能會造成:

  1. 程式碼超冗長 :中間層組件變成「傳話筒」,明明不需要這些資料卻要幫忙傳遞
  2. 改東西超麻煩:要加一個新的全域狀態?準備改一堆檔案吧
  3. 重構變地獄:想調整組件結構?先把 props 傳遞鏈理清楚再說
  4. 性能莫名其妙變差 : 不相關的 props 變動也會讓組件重新渲染

What is Provider Context

Context 是 React 提供的一種「跨組件通訊」機制。

Provider 就像廣播電台,負責發送訊號(提供資料);Consumer(通常用 useContext)就像收音機,負責接收訊號(使用資料)。而 Context 本身就是那個頻道,確保大家收聽的是同一個電台。這種方式特別適合那些「很多組件都需要」的資料,像是使用者資訊、主題設定、語言偏好等等。

Context 的三劍客

  1. Context 本體:用 createContext() 建立的神奇盒子
  2. Provider:負責把資料塞進盒子裡
  3. Consumer : 從盒子裡拿資料的方式(通常用 useContext)

基本套路就是這樣:

// 1. 先建立一個 Context 
const ThemeContext = createContext(); 
 
// 2. 用 Provider 包起來,餵資料進去 
function ThemeProvider({ children }) { 
  const [theme, setTheme] = useState('light'); 
  return ( 
    <ThemeContext.Provider value={{ theme, setTheme }}> 
      {children} 
    </ThemeContext.Provider> 
  ); 
} 
 
// 3. 需要用的時候就拿出來 
function SomeComponent() { 
  const { theme, setTheme } = useContext(ThemeContext); 
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> 
    切換主題 
  </button>; 
}

讓我們把前面那個災難用 Provider 重構一下。首先,我們要先建立好 Provider 和 Context 的內容:

// UserContext.js 
const UserContext = createContext(); 
 
export const UserProvider = ({ children }) => { 
  const [user, setUser] = useState(null); 
   
  return ( 
    <UserContext.Provider value={{ user, setUser, isLoggedIn: !!user }}> 
      {children} 
    </UserContext.Provider> 
  ); 
}; 
 
export const useUser = () => useContext(UserContext); 
 
// ThemeContext.js   
const ThemeContext = createContext(); 
 
export const ThemeProvider = ({ children }) => { 
  const [theme, setTheme] = useState('light'); 
   
  return ( 
    <ThemeContext.Provider value={{ theme, setTheme }}> 
      {children} 
    </ThemeContext.Provider> 
  ); 
}; 
 
export const useTheme = () => useContext(ThemeContext);

接著組合一下:

function AppProviders({ children }) { 
  return ( 
    <UserProvider> 
      <ThemeProvider> 
        <CartProvider> 
          {children} 
        </CartProvider> 
      </ThemeProvider> 
    </UserProvider> 
  ); 
} 
 
// 重構後的 App - 超清爽! 
function App() { 
  return ( 
    <AppProviders> 
      <Layout /> 
    </AppProviders> 
  ); 
} 
 
// Layout 不用再當傳話筒了 
function Layout() { 
  return ( 
    <div> 
      <Header /> 
      <Main /> 
      <Footer /> 
    </div> 
  ); 
} 
 
// Header 想要什麼資料就自己拿 
function Header() { 
  const { user } = useUser(); 
  const { theme } = useTheme(); 
   
  return <header className={theme}>歡迎 {user?.name}!</header>; 
}

是不是瞬間清爽很多?


小心!別讓 Provider 打架 ⚠️

有時候在複雜的應用裡,你可能會意外地建立巢狀的 Provider,就像這樣:

function AppProviders({ children }) { 
  return ( 
    <UserProvider> 
      <ThemeProvider> 
        <CartProvider> 
          {children} 
        </CartProvider> 
      </ThemeProvider> 
    </UserProvider> 
  ); 
} 
 
function App() { 
  return ( 
    <AppProviders> 
      <Header /> 
      <Content /> 
      <Footer /> 
    </AppProviders> 
  ); 
} 
 
function Layout() { 
  return ( 
    <UserProvider /* 糟糕!又有一個 UserProvider */> 
      <Main /> 
    </UserProvider> 
  ); 
}

這會造成內層的組件拿到內層 Provider 的資料,外層的組件拿到外層 Provider 的資料,就會造成不預期的結果。像是我在外層的 UserProvider 寫 user name 是 Anthea,內層的 UserProvider 是香菜,這時候我在 Header 拿到的 user name 和 Main 拿到的 user name 就會不一樣。

要怎麼避免?

如果想避免這種情況,最簡單的就是加個檢查機制:

const checkNestedProvider = <T extends any>(context: React.Context<T | null>) => { 
    const existingContext = useContext(context); 
    if (existingContext !== null) { 
        throw new Error("Provider cannot be nested. A provider is already present in the component tree."); 
    } 
}; 
 
// 在 Provider 裡面用 
export const UserProvider = ({ children }) => { 
  checkNestedProvider(UserContext); // 檢查有沒有重複 
  const [user, setUser] = useState(null); 
   
  return ( 
    <UserContext.Provider value={{ user, setUser }}> 
      {children} 
    </UserContext.Provider> 
  ); 
};

這樣如果有人不小心建立了巢狀 Provider,就會立刻收到錯誤訊息,避免奇怪的 bug!


用了 Provider Context 之後,你會發現:

  • 再也不用玩傳話遊戲:直接從 Context 拿資料,超方便
  • 程式碼變得超乾淨:中間層組件不用再當傳話筒
  • 維護變簡單:要改全域狀態只要改 Provider 就好
  • 重構不再是惡夢 : 組件之間的耦合度大幅降低

當然,Context 也不是萬能的。如果只是簡單的父子組件傳資料,直接用 props 可能更直觀;如果是超複雜的狀態管理,可能需要 Redux 或 Zustand 更容易處理。

但在大部分情況下,Context 都是個很棒的選擇。它是 React 內建的,學會了就能在各種專案裡使用,而且概念簡單、上手容易。

以上就是今天的分享,我們下次見 👋🏻