React上下文

Context提供了一种通过组件树传递数据的方法,而不必在每个级别手动传递props。

在典型的React应用程序中,数据通过属性自顶向下(父级到子级)传递,但对于应用程序中的许多组件都需要的某些类型的属性(例如locale preference、UI主题)来说,这可能很麻烦。Context提供了一种在组件之间共享这些值的方法,而不必显式地在树的每一层传递属性。

什么时候使用Context

上下文的设计目的是共享可以被认为是React组件树的“全局”数据,比如当前经过身份验证的用户、主题或首选语言。例如,在下面的代码中,我们手动传递一个“theme”属性,以便给按钮组件添加样式:

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

function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

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

使用上下文,我们可以避免通过中间元素传递属性:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current **value**.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

使用上下文之前

上下文主要用于在不同嵌套级别的许多组件需要访问某些数据时。谨慎地应用它,因为它使组件重用更加困难。

如果只想避免通过多个级别传递一些道具,组件组合通常是比上下文更简单的解决方案。

例如,一个Page组件跨级别传递useravatarSize属性,以便内嵌的LinkAvatar组件可以获取到他们。

<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

如果最终只有Avatar组件需要useravatarSize属性,那么将这两个属性传递给多个级别可能会让人觉得多余。同样令人恼火的是,每当Avatar组件需要更多来自顶层的属性时,你也必须在所有的中间层添加它们。

除了上下文之外的一个解决方式是,将Avatar组件本身向下传递,这样中间组件就不需要关心useravatarSize属性。

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// Now, we have:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}

通过这种改动,只有最顶层的Page组件需要了解LinkAvatar组件关于useravatarSize属性的使用。

这种控制反转在很多情况下可以使代码更简洁,因为它减少了需要通过应用程序传递的属性数量,并为根组件提供了更多的控制。然而,在很多情况下,这都不是正确的选择:将复杂度在树中上移让高级别的组件看起来更复杂难懂,同时也迫使较低级别的组件比您可能希望的要更灵活。

您并不局限于一个组件的单个子组件。您可以传递多个子节点,甚至可以为子节点提供多个单独的“插槽”,如下所示:

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}
    />
  );
}

在许多情况下,当您需要将一个孩子与其直系父母解耦时,这种模式就足够了。如果子元素在呈现之前需要与父元素进行通信,则可以使用render props进行进一步的处理。

然而,有时相同的数据需要由树中的许多组件访问,并在不同的嵌套级别访问。Context允许您将这些数据“广播”给下面的所有组件,并对其进行更改。使用上下文可能比其他方法更简单的常见示例包括管理当前地区、主题或数据缓存。

API

React.createContext

const MyContext = React.createContext(defaultValue);

创建上下文对象。当React渲染的组件订阅此上下文对象时,它将从树中与其最匹配的Provider处读取当前上下文值。

defaultValue参数仅当组件在树中没有匹配的提供程序时才使用。这有助于在不封装组件的情况下独立测试组件。注意:传递undefined作为提生产者的值并不会导致使用组件使用defaultValue

Context.Provider

<MyContext.Provider value={/* some value */}>

每个上下文对象都带有一个Provider React组件,该组件允许消费者组件订阅上下文更改。

Provider接收一个value属性,并将他传给后继的消费者组件。一个生产者可以和多个消费者建立连接。生产者可以被嵌套,用以覆盖树中更深的值。

无论何时,当生产者的value值改变时,它的后继消费者组件都会被重新渲染。生产者到消费者之间的传递并不受shouldComponentUpdate方法支配,因此,即使祖先组件退出更新,使用者也会被更新。

更改是通过使用与Object.is相同的算法比较新值和旧值来确定的。

注意:在将对象作为value传递时,确定更改的方法可能会导致一些问题:请参阅警告。

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

可以为类上的contextType属性分配一个由response.createcontext()创建的上下文对象。这允许您使用this.context使用该上下文类型最近的当前值。您可以在任何生命周期方法(包括render函数)中引用它。

注意:您只能使用此API订阅单个上下文。如果需要读取多个上下文,请参阅Consuming Multiple Contexts。如果你正在使用实验性的public class fields syntax,可以使用静态类字段初始化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 => /* render something based on the context value */}
</MyContext.Consumer>

订阅上下文更改的React组件。这允许您订阅函数组件中的上下文。

需要一个函数作为子节点。函数接收当前上下文值并返回一个React节点。传递给函数的value参数将等于树中此上下文的最近提供者的value属性。如果上面没有此上下文的生产者,value参数将等于传递给createContext()defaultValue

注意:有关“函数为子节点”的更多信息,请参见render props。

Context.displayName

上下文对象接收一个displayName属性。React DevTools使用这个字符串来确定要为上下文显示什么。

例如,以下组件将作为MyDisplayName出现在DevTools中:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

Examples

Dynamic Context

关于主题的有动态值得更复杂例子:

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';

// An intermediate component that uses the 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() {
    // The ThemedButton button inside the ThemeProvider
    // uses the theme from state while the one outside uses
    // the default 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);

内嵌组件中更新上下文

通常需要从嵌套在组件树中某个位置的组件更新上下文。在这种情况下,您可以通过上下文向下传递一个函数,以允许消费者更新上下文:

theme-context.js
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // The Theme Toggler Button receives not only the theme
  // but also a toggleTheme function from the context
  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 also contains the updater function so it will
    // be passed down into the context provider
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // The entire state is passed to the provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

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

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

使用多个上下文

为了保持上下文的快速重新呈现,React需要使每个上下文使用者成为树中的一个单独节点。

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // App component that provides initial context values
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

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

// A component may consume multiple contexts
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

如果经常同时使用两个或多个上下文值,您可能需要考虑创建自己的渲染属性组件,该组件同时提供这两个值。

警告

因为上下文使用引用标识来确定何时重新渲染,所以当提供者的父类重新渲染时,可能会触发使用者中的一些意外渲染。例如,下面的代码将在每次提供者重新渲染时重新渲染所有消费者,因为总是为value创建一个新对象:

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

要解决这个问题,将value提升到父级的状态:

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

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

原文地址:https://reactjs.org/docs/context.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值