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這樣可能會造成:
- 程式碼超冗長 :中間層組件變成「傳話筒」,明明不需要這些資料卻要幫忙傳遞
- 改東西超麻煩:要加一個新的全域狀態?準備改一堆檔案吧
- 重構變地獄:想調整組件結構?先把 props 傳遞鏈理清楚再說
- 性能莫名其妙變差 : 不相關的 props 變動也會讓組件重新渲染
What is Provider Context
Context 是 React 提供的一種「跨組件通訊」機制。
Provider 就像廣播電台,負責發送訊號(提供資料);Consumer(通常用 useContext)就像收音機,負責接收訊號(使用資料)。而 Context 本身就是那個頻道,確保大家收聽的是同一個電台。這種方式特別適合那些「很多組件都需要」的資料,像是使用者資訊、主題設定、語言偏好等等。
Context 的三劍客
- Context 本體:用
createContext()建立的神奇盒子 - Provider:負責把資料塞進盒子裡
- 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 內建的,學會了就能在各種專案裡使用,而且概念簡單、上手容易。
以上就是今天的分享,我們下次見 👋🏻