[譯]React高級指引3:Context

原文連接:reactjs.org/docs/contex…html

引言

context提供了一種數據傳輸方式,它使得數據能夠直接經過組件樹傳遞而不須要在每個層級上手動地傳遞props。react

在典型的React應用中,數據是經過props自上而下(父組件傳遞給子組件)傳遞的,可是對於同時被許多組件所須要的某些props(如我的偏好,UI主題)來講,使用這種方式傳遞數據簡直就是受刑。Context提供了不須要顯式地在組件樹上每一個層級傳遞prop而是直接在組件之間傳遞的方法。算法

何時使用context

context設計的目的是爲了共享那些對於組件樹而言是「全局」的數據,好比當前用戶信息,主題或語言等。在下面的示例代碼中,咱們手動傳遞了一個「theme」prop來爲Button組件提供樣式。緩存

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  //Toolbar組件必需要傳遞一個額外
  //的prop「theme」給ThemedButton組件。
  //若是應用中的每一個按鈕都須要知道theme是
  //什麼的話,那麼這會要人老命的,由於你須要在全部
  //組件中一個個傳遞。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}
複製代碼

可是使用contex的話,咱們能夠避免經過中間組件來傳遞props:bash

//context讓咱們不須要在每個組件中顯式地傳遞prop就
//能夠之間將數據傳遞進位於組件樹深層次的組件中。
//建立一個表示當前主題的context並賦予初始值light
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    //使用Provider將當前主題傳遞到當前組件樹之下。
    任何組件均可以獲取到這個值,不管它的層級有多深。
    //在本例中咱們把「dark「做爲當前主題傳遞。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
//位於中間層級的組件不須要再顯式地傳遞主題了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  //將當前主題的contex值賦值給contextType
  //React會在當前層級之上找到最近的Provider並獲取
  //它的值。在本例中,當前主題是」dark"。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } } 複製代碼

使用context以前的考慮

context主要在位於不一樣嵌套層級的組件須要獲取同一個數據是使用。請謹慎地使用它由於context會使你的組件複用度變差。babel

若是你僅僅只想避免在過多的層級傳遞prop,那麼組件組合是比context更簡單的解決方案。數據結構

好比在下面的例子中,Page組件傳遞了useravatarSize給了幾個層級之下的LinkAvatar組件:app

<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組件使用了useravatarSize,那麼你可能會以爲把它們傳遞那麼多層級徹底不必。若是Avatar組件須要從頂層傳遞更多的prop,那麼你可能會所以抓狂,由於你須要同時在全部的中間組件上都添加這些prop一遍。ide

不適用context解決這個問題的方法是把Avatar組件做爲prop傳遞下去,這樣中間組件就不須要知道其餘關於user或avatarSize的信息了:函數

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}
複製代碼

如今,只有最頂層的Page組件須要知道Link組件和Avatar組件須要使用useravatarSize

這對組件的控制反轉經過減小傳遞的prop的數量以及對跟組件的更多控制使你的代碼更加簡潔。可是這並不適用於全部狀況,讓複雜的邏輯位於高層級組件會使得它們變得複雜而且強制讓低層級組件適應這種狀況可能不是你想要的。

你的組件並不限制於只能接收一個子組件,你能夠在組件中傳遞多個子組件,甚至爲子組件封裝多個插槽(slots),正如文檔中所舉例的

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}
    />
  );
}
複製代碼

上述模式適用於大部分場景,在這些場景下你須要將子組件和直接父母組件解耦。若是子組件在渲染以前組要和父組件交互,你能夠在這篇文章中獲取相關知識。

而後有些時候某一數據被在組件樹不一樣嵌套層級的組件所須要。context可以讓你「廣播」這些數據,因此在這種狀況下請直接使用context。使用context的場景一般是管理locale,theme和一些緩存數據,這比使用替代方案簡單的多。

API

React.createContext

const MyContext = React.createContext(defaultValue);
複製代碼

上述代碼建立了一個context對象。當React渲染的組件使用了這個context對象時,React會從當前層級之上匹配最近的一個Provider來讀取該context值。

參數defaultValue只在組件沒有在上層組件樹中找到匹配的Provider纔會生效。這有助於在不包裝組件的狀況下測試它們。注意:傳遞undefined給Provider時消費組件的defaultValue不會生效。

Context.Provider

<MyContext.Provider value={/* 某個值 */}>
複製代碼

每個context對象都返回一個React組件,它容許消費組件實時更新值的變化。

Provider接收一個value做爲prop並將其傳遞給它的子消費組件。一個Provider能夠和多個消費組件有對應關係。Provider之間也能夠相互嵌套而且深層次的value值會覆蓋其餘的值。

當Provider的value值更新時,它內部的全部消費組件都會從新渲染。Provider和它內部組件的value值傳遞不受限於shouldComponentUpdate函數,所以當消費組件的祖先組件中止更新時它也能夠更新。

根據新舊值來決定是否更新使用的是與Object.is相同的算法。

注意: 當傳遞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;
複製代碼

class的contextType屬性會被重賦值爲一個經過React.createContext()建立的context對象。這能讓你經過使用this.context來消費最近的context上的值。你能夠在任何生命週期方法中使用它,包括render方法。

注意: 經過這個API你只能訂閱一個context。若是你須要訂閱多個context,請查看使用多個context。 若是你在使用實驗性的public class fileds語法,你可使用static這個類屬性來初始化你的contextType

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}
複製代碼

Context.Consumer

<MyContext.Consumer>
  {value => /*根據context值渲染一些數據 */}
</MyContext.Consumer>
複製代碼

這裏,React組件也能夠獲取到context的變動,這能讓你在函數式組件中完成訂閱。

須要函數做爲子元素。這個函數接收當前context的值並返回一個React的節點。value入參的值等同於高層級上最近的Provider的value值。若是在更高層級上沒有對應的Provider,那麼value入參的值等同於構建context時傳入的defaultValue值。

注意: 須要瞭解關於函數做爲子元素模式的更多內容,請查看render props

Context.displayName

context對象接收一個displayName字符串屬性。React DevTools根據這個字符串來決定context要顯式的值。

好比,下面的組件會在DevTools上顯示MyDisplayName :

const MyContext = React.createContext(/* 一些值 */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in 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 // default value
);
複製代碼

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,而在Provider外部
    //的按鈕使用了默認的dark 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傳遞一個函數來讓消費組件更新context:

theme-context.js

//確保傳遞給createContext的數據結構與消費組件
//所須要的相匹配
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
複製代碼

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  //ThemeToggleButton組件不只從context接收了theme
  //還接收了toggleTheme函數
  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,
    };
  }

  render() {
    // 整個state都被provider傳遞下去
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

Consuming Multiple Contexts 
複製代碼

使用多個context

爲了保證context能快速地從新渲染,React須要每個consumer組件的context成爲組件樹上單獨的節點。

// Theme context,默認值爲light
const ThemeContext = React.createContext('light');

// 登陸用戶context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;
    //App組件提供了context的初始值
    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>
  );
}
複製代碼

若是兩個以上的context值常常被一塊兒使用,你就能夠考慮構建可以同時提供這些值的渲染組件。

注意事項

由於context使用參考標識(reference identity)來決定何時渲染,這裏可能會有一些陷阱,當provider的父組件從新渲染時,可能在consumer組件中觸發一些無心識的渲染。好比下面的代碼中每一次Provider從新渲染時因爲value屬性都會被賦予一個新的對象,在它之下的全部consumer組件都會從新渲染:

class App extends React.Component {
  render() {
    return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}
複製代碼

爲了不這一點,把value值提高到父組件的state中:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}
複製代碼

過期的API

注意: 先前React使用實驗性的context API運行。老版的API在全部的16.x版本中都會獲得支持,但用到它的應用應該遷移到新的版本。過期的API將會在將來的版本中被移除。閱讀過期的context文檔瞭解更多。

相關文章
相關標籤/搜索