天天看點

Context(精讀React官方文檔—15)

Context有什麼作用?

官方描述:Context 提供了一個無需為每層元件手動添加 props,就能在元件樹間進行資料傳遞的方法。如果屬性隻能通過props從上往下進行傳遞,在有些應用場景下是非常繁瑣的,比如UI主題的繼承,Context 提供了一種在元件之間共享此類值的方式,而不必顯式地通過元件樹的逐層傳遞 props。

解讀

  1. 通過Context可以讓元件樹之間進行資料傳遞,并且不需要手動添加props.

何時使用Context?

Context設計的目的是為了共享那些對于一個元件樹而言是全局的資料。

例如下面的例子,通過theme屬性手動調整一個按鈕元件的樣式

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}
function Toolbar(props) {
  // Toolbar 元件接受一個額外的“theme”屬性,然後傳遞給 ThemedButton 元件。
  // 如果應用中每一個單獨的按鈕都需要知道 theme 的值,這會是件很麻煩的事,
  // 因為必須将這個值層層傳遞所有元件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}
class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}
複制代碼      
  • 通過props可以避免中間元素傳遞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 (
    <div>
      <ThemedButton />
    </div>
  );
}
class ThemedButton extends React.Component {
  // 指定 contextType 讀取目前的 theme context。
  // React 會往上找到最近的 theme Provider,然後使用它的值。
  // 在這個例子中,目前的 theme 值為 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
複制代碼      

使用Context之前的考慮

  • 帶來的問題
Context的主要應用場景是不同層級的元件需要通路一些相同的資料,這可能會使得元件的複用性變差。
  • 層層傳遞props帶來的麻煩
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>
複制代碼      

如果在最後隻有 Avatar 元件真的需要 user 和 avatarSize,那麼層層傳遞這兩個 props 就顯得非常備援。而且一旦 Avatar 元件需要更多從來自頂層元件的 props,你還得在中間層級一個一個加上去,這将會變得非常麻煩。

  • 一種無需Context的解決方案(将Avatar元件自身傳遞下去)
function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}
// 現在,我們有這樣的元件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}
複制代碼      
  • 上面做法帶來的問題
  1. 使得高層元件更加複雜。
  • 傳遞多個元件的情況
function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}
複制代碼      

API

React.createContext

const MyContext = React.createContext(defaultValue);
複制代碼      
建立一個 Context 對象。當 React 渲染一個訂閱了這個 Context 對象的元件,這個元件會從元件樹中離自身最近的那個比對的 Provider 中讀取到目前的 context 值。

注意事項

隻有當元件所處的樹中沒有比對到 Provider 時,其 defaultValue 參數才會生效。此預設值有助于在不使用 Provider 包裝元件的情況下對元件進行測試。注意:将 undefined 傳遞給 Provider 的 value 時,消費元件的 defaultValue 不會生效。

Context.Provider

<MyContext.Provider value={/* 某個值 */}>
複制代碼      
  1. 每個Context對象都會傳回一個Provider元件,這個元件允許訂閱context對象的變化。
  2. Provider接收一個value屬性,傳遞給消費元件。
  3. Provider中的value值發生變化的時候,其所有的消費元件都會重新渲染。

Class.contextType

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 元件的值進行渲染 */
  }
}
MyClass.contextType = MyContext;
複制代碼      
挂載在類上的contextType屬性會被重新指派為一個有React.createContext()建立的Context對象。這個屬性能夠讓我們使用this.context來使用最近Context上的那個值。
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基于這個值進行渲染工作 */
  }
}
複制代碼      

Context.Consumer

  • 這個API可以讓函數式元件使用Context,相當于類元件的this.context
<MyContext.Consumer>
  {value => /* 基于 context 值進行渲染*/}
</MyContext.Consumer>
複制代碼      

Consumer内部的函數接收目前的context值,然後傳回React節點,傳遞給函數的value值等價于元件樹上方離這個context最近的Provider提供的value值。

Context.displayName

這個屬性是用來輔助Reacat DevTools來使用字元串确定context要顯示的内容的。
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
複制代碼      

示例

動态Context

對上面的theme例子使用動态值。

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};
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;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
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.changeTheme}>
      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);
複制代碼      

在嵌套元件中更新Context

在嵌套很深的元件中更新context是很必要的,可以通過context傳遞函數,是的consumers元件更新context

// 確定傳遞給 createContext 的預設值資料結構是調用的元件(consumers)所能比對的!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
複制代碼      

theme-toggle-button.js

import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
  // Theme Toggler 按鈕不僅僅隻擷取 theme 值,
  // 它也從 context 中擷取到一個 toggleTheme 函數
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}
export default ThemeTogglerButton;
複制代碼      

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,
    };
  }
  render() {
    // 整個 state 都被傳遞進 provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}
function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}
ReactDOM.render(<App />, document.root);
複制代碼      

消費多個Context

為了確定context快速進行重渲染,React需要使每一個消費元件的context在元件樹中成為一個單獨的節點。
// Theme context,預設的 theme 是 “light” 值
const ThemeContext = React.createContext('light');
// 使用者登入 context
const 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 (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}
// 一個元件可能會消費多個 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}
複制代碼      

  • 下面的代碼,當每一次Provider重渲染時,會重渲染下面的所有消費者元件,因為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);
    this.state = {
      value: {something: 'something'},
    };
  }
  render() {
    return (
      <MyContext.Provider value={this.state.value}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}
複制代碼      

歡迎大家關注我的專欄,一起來學習React!

繼續閱讀