在react中,context是一種無需為每層組件手動添加props就能在組件樹之間進行資料傳遞的方法;context提供了一種在組件之間共享指定值的方式,而且不必顯式的通過組件樹的逐層傳遞props。
本教學操作環境:Windows10系統、react17.0.1版、Dell G3電腦。
Context 提供了一個不需要為每層元件手動新增props,就能在元件樹間進行資料傳遞的方法。在典型的React 應用程式中,資料是透過props 屬性自上而下(由父及子)進行傳遞的,但這種做法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI主題),這些屬性是應用程式中許多元件都需要的。 Context 提供了一種在元件之間共用此類值的方式,而不必明確地透過元件樹的逐層傳遞props。
Context 設計目的是為了共享那些對於一個元件樹而言是「全域」的數據,例如當前認證的使用者、主題或首選語言。舉個例子,在下面的程式碼中,我們透過一個「theme」 屬性手動調整一個按鈕元件的樣式
class App extends React.Component { render() { return <Toolbar theme="dark" />; }}function Toolbar(props) { // Toolbar 元件接受一個額外的「theme」屬性,然後傳遞給ThemedButton 元件。 // 如果應用程式中每一個單獨的按鈕都需要知道theme 的值,這會是件很麻煩的事, // 因為必須將這個值層層傳遞所有元件。 return ( <p> <ThemedButton theme={props.theme} /> </p> );}class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } }// 透過props傳遞:App -> Toolbar -> ThemedButton// 如果嵌套很深,那麼需要逐層傳遞props,即使中間不需要該props,顯得很繁瑣使用context, 我們可以避免透過中間元素傳遞props
// Context 可以讓我們無須明確地傳遍每一個元件,就能將值深入傳遞進元件樹。 // 為目前的theme 建立一個context("light"為預設值)。 const ThemeContext = React.createContext('light');class App extends React.Component { render() { // 使用一個Provider 來將目前的theme 傳遞給以下的元件樹。 // 無論多深,任何元件都能讀取這個值。 // 在這個範例中,我們將「dark」 作為目前的值傳遞下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); }}// 中間的元件再也不必指明往下傳遞theme 了。 function Toolbar() { return ( <p> <ThemedButton /> </p> );}class ThemedButton extends React.Component { // 指定contextType 讀取目前的theme context。 // React 會往上找到最近的theme Provider,然後使用它的值。 // 在這個範例中,目前的theme 值為「dark」。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; }}// 也可以使用ThemedButto.contextType = ThemeContext;建立一個Context 物件。當React 渲染一個訂閱了這個Context 物件的元件,這個元件會從元件樹中離自身最近的那個符合的Provider 中讀取到目前的context 值。
只有當組件所處的樹中沒有符合到Provider 時,其defaultValue 參數才會生效。這有助於在不使用Provider 包裝組件的情況下對組件進行測試。注意:將undefined 傳遞給Provider 的value 時,消費組件的defaultValue 不會生效。
每個Context 物件都會傳回一個Provider React 元件,它允許消費元件訂閱context 的變更。
Provider 接收一個value 屬性,傳遞給消費元件。一個Provider 可以和多個消費組件有對應關係。多個Provider 也可以嵌套使用,裡層的會覆蓋外層的資料。
當Provider 的value 值改變時,它內部的所有消費元件都會重新渲染。 Provider 及其內部consumer 元件都不受制於shouldComponentUpdate 函數,因此當consumer 元件在其祖先元件退出更新的情況下也能更新。
掛載在class 上的contextType 屬性會被重賦值為一個由React.createContext() 所建立的Context 物件。這能讓你使用this.context 來消費最近Context 上的那個價值。你可以在任何生命週期中存取它,包括render 函數中
import MyContext from './MyContext';class MyClass extends React.Component { componentDidMount() { let value = this.context; /* 在元件掛載完成後,使用MyContext 元件的值來執行一些有副作用的操作*/ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* 基於MyContext 元件的值進行渲染*/ } // 或如上邊範例一樣使用static contextType = MyContext;}MyClass.contextType = MyContext;這裡,React 元件也可以訂閱到context 變更。這能讓你在函數式元件中完成訂閱context。
這需要函數作為子元素(function as a child)這種做法。這個函數接收目前的context 值,並傳回一個React 節點。傳遞給函數的value 值等同於往上元件樹離這個context 最近的Provider 提供的value 值。如果沒有對應的Provider,value 參數等同於傳遞給createContext() 的defaultValue。
context 物件接受一個名為displayName 的property,類型為字串。 React DevTools 使用該字串來決定context 要顯示的內容。
如下述元件在DevTools 中將顯示為MyDisplayName
const MyContext = React.createContext(/* some value */);MyContext.displayName = 'MyDisplayName';<MyContext.Provider> // "MyDisplayName.Provider" 在DevTools <MyContext.Consumer> // "DisplayName.Consumer"在DevTools 中對於上面的theme 例子,使用動態值(dynamic values)後更複雜的用法
theme-context.js
export const themes = { light: { foreground: '#000000', background: '#eeeeee', }, dark: { foreground: '#ffffff', background: '#22222', },};export const ThemeContext = Reactground: '#22222', },};export const ThemeContext = React .createContext(themes.dark); // 該處為預設值themed-button.js
import { ThemeContext } from './theme-context';class ThemedButton extends React.Component { render() { let props = this.props; // 取得到ThemeContext中的預設值let theme = this.context; return ( < button {...props} style={{backgroundColor: theme.background}} /> ); } // static contextType = ThemeContext;}ThemedButton.contextType = ThemeContext;export default ThemedButton;app.js
import { ThemeContext, themes } from './theme-context';import ThemedButton from './themed-button';// 一個使用ThemedButton 的中間元件function Toolbar(props) { return ( <ThemedButton onClick={props.Theme }> Change Theme </ThemedButton> );}class App extends React.Component { constructor(props) { super(props); this.state = { theme: themes.light, }; this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; } render() { // 在ThemeProvider 內部的ThemedButton 按鈕元件使用state 中的theme 值, // 而外部的元件使用預設的theme 值return ( <Page> <ThemeContext.Provider value={this.state.theme}> <Toolbar changeTheme={this.toggleTheme} /> </ThemeContext .Provider> <Section> <ThemedButton /> </Section> </Page> ); }}ReactDOM.render(<App />, document.root);// 使用ThemeContext.Provider包裹的元件,可以消費到ThemeContext中的value// 即Toolbar、ThemedButton中都可以使用this.context來獲取到value// 注意觀察,更新state的方法是透過props向下傳遞,由子孫元件觸發更新,下面會講到透過context的方式傳遞更新函數在上面的例子中,我們透過props 的方式向下傳遞一個更新函數,從而改變App 中themes 的值。我們知道,從一個在元件樹中嵌套很深的元件中更新context 是很有必要的。在這種場景下,你可以透過context 傳遞一個函數,使得consumers 元件更新context
theme-context.js
// 確保傳遞給createContext 的預設值資料結構是呼叫的元件(consumers)所能匹配的! export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, // 定義更新主題的方法,向下傳遞});theme-toggler-button.js
import { ThemeContext } from './theme-context';function ThemeTogglerButton() { // Theme Toggler 按鈕不只取得theme 值,它也從context 取得toggleTheme 函式(下面app.js部分) return ( < ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> ); }export default ThemeTogglerButton;app.js
import { ThemeContext, themes } from './theme-context';import ThemeTogglerButton from './theme-toggler-button';class App extends React.Component { constructor(props) { super(props); this.toggleTheme = ( ) => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; // State 也包含了更新函數,因此它會傳遞進context provider。 this.state = { theme: themes.light, toggleTheme: this.toggleTheme, // 定義更新函數,透過context方式向下傳遞}; } render() { // 整個state 都被傳遞進provider return ( <ThemeContext. Provider value={this.state}> <Content /> </ThemeContext.Provider> ); }}function Content() { return ( <p> <ThemeTogglerButton /> </p> );}ReactDOM.render(<App />, document.root);為了確保context 快速進行重渲染,React 需要讓每個consumers 元件的context 在元件樹中成為一個單獨的節點
// Theme context,預設的theme 是"light" 值const ThemeContext = React.createContext('light');// 使用者登入contextconst UserContext = React.createContext({ name: 'Guest',});class App extends React .Component { render() { const { signedInUser, theme } = this.props; // 提供初始context 值的App 元件return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> < Layout /> </UserContext.Provider> </ThemeContext.Provider> ); }}function Layout() { return ( <p> <Sidebar /> <Content /> </p> );}// 一個元件可能會消耗多個contextfunction Content() { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext. Consumer> )} </ThemeContext.Consumer> );}如果兩個或更多的context 值經常被一起使用,那麼你可能要考慮一下另外創建你自己的渲染組件,以提供這些值。
因為context 會使用參考識別(reference identity)來決定何時進行渲染,這裡可能會有一些陷阱,當provider 的父元件進行重渲染時,可能會在consumers 元件中觸發意外的渲染。舉個例子,當每一次Provider 重渲染時,以下的程式碼會重渲染所有下面的consumers 元件,因為value 屬性總是被賦值為新的對象
class App extends React.Component { render() { return ( <MyContext.Provider value={{something: 'something'}}> <Toolbar /> </MyContext.Provider> ); }}為了防止這種情況,將value 狀態提升到父節點的state 裡
class App extends React.Component { constructor(props) { super(props); // 多次渲染,state 會被保留,當value不變時,下面的consumers 元件不會重新渲染this.state = { value: { something: 'something'}, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); }}