I Want To Know React - Context 范例 & 使用技巧

回顾 Context 语法

上一章节中,我们介绍了 Context 的使用简介与语法。

React context 的使用环绕三个角色在运作:

  • Context object:代表 context 本身
  • Provider:用来提供 context 值
  • Consumer:用来使用 context 值

另外,我们也介绍了以下几个 context 语法:

  • React.creatContext
  • Context.Provider
  • Context.Consumer
  • Class.contextType
  • Context.displayName

在下一章中,我们将介绍一些 context 的范例以及使用技巧。

Context 实际范例

现在,让我们使用实际的范例来了解 context 如何使用。先来了解一下这个范例要达到哪些需求。

需求

现在要做一个小页面,此页面可以设定 UI theme。页面中有两个 button:

  • 一颗 button 在工具栏中,跟着 UI theme 改变颜色。另外,只要点击此 button,就可切换 UI theme
  • 一颗 button 在工具栏外,不会跟着 UI theme 改变颜色

范例

我们如下实作需求:

首先先定义 theme 的种类,这个 app 中会有 lightdark 两种 theme

另外,而此页面的预设 theme 为 dark

const themes = {
  light: {
    background: "#eeeeee",
    color: "#000000"
  },
  dark: {
    background: "#222222",
    color: "#ffffff"
  }
};

const ThemeContext = React.createContext(
  themes.dark // default value
);

创建 context 的语法为 React.createContext(),参数带入 theme.dark 代表预设的 theme。

接着制作一个可以随着 theme 的变化而改变的 button:

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{ backgroundColor: theme.background, color: theme.color }}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;

ThemeButton 采用 Class.contextType 语法,将要使用的 context 设定为 ThemeContext

因为要把 button 的背景与字体颜色都根据 context 的内容调整,因此在 ThemeButton 中用 this.context 的语法取得 theme 并设为 button 的 backgroundColorcolor

最後就来实作 Toolbar 与最重要的 App component:

// An intermediate component that uses the ThemedButton
function Toolbar(props) {
  return (
    <div className="toolbar">
      <ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>
    </div>
  );
}

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 (
      <div>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <ThemedButton>Outside Button</ThemedButton>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

Toolbar 职责很简单,只显示一个外框,并负责传递 changeTheme 的事件。

App component 则负责:

  • 使用 ProviderToolbar 以下的 component 定为 light theme
  • Render Toolbar 与外层的 button
  • 实作转换 theme 的函式 toggleTheme

读者也可以到 CodePen 上参考完整范例。

从结果上可以看到,Toolbar 内的 button 使用了 Provider 提供的 theme context light,它会随着 theme 的切换而改变颜色。相较之下,Toolbar 外的 button 没有 Provider 包覆,取得的 theme context 会是预设值 dark,其颜色不会因为 Provider 的 context 值改变而有所变动。

从巢状内层的 component 中触发 context 更新

接下来来学习如何从巢状内的 component 中触发 context 更新。

读者应该会有一个疑问,context 确实可以将值不透过中间层的 component 就传递到底层的 component,然而如果底层的 component 要触发更新时要怎麽办呢?是否还是要将 context 更新函式也透过每一个中间层 component 传下去呢?这样是否就失去 context 的作用了呢?

技巧:将 context 更新函式设为 context 内容之一

让巢状内层的 component 也可不经过中间层 component 就触发更新的方法其实很简单,把 context 更新函式也设为 context 内容之一即可。

也就是如果要让 context 可以被内层 component 改变的话,context 就要有以下的内容:

  • Context 实际要提供的值
  • 更新 context 值的函式

接着就让我们来看看实际范例。

需求

接续刚刚的范例,我们有一个可以设定 UI theme 的页面,然而此页面只会有一个可以随着 UI theme 而改变颜色的 button。

但在程序面有另一个需求:

  • 要让中间层的 component Toolbar 不用传递 toggleTheme prop 也能够让最下层的 button 可以改变 theme。

范例

实作方式如下,此范例会根据上个范例的内容修改,只会记录有改动的部分:

首先,修改 ThemeContext

// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme 的内容也是一样会有 lightdark,预设的 theme 也是 dark

然而不同的是,因为要让下层的 button 不经过中间层 component 就可以改变 theme,因此额外把 context 更新函式 toggleTheme 也加到 context 中。

接着修改 ThemeButton

class ThemeTogglerButton extends React.Component {
  render() {
    let props = this.props;
    // The Theme Toggler Button receives not only the theme
    // but also a toggleTheme function from the context
    let { theme, toggleTheme } = this.context;
    return (
      <button
        {...props}
        onClick={toggleTheme}
        style={{ backgroundColor: theme.background, color: theme.color }}
      >
        Toggle Theme
      </button>
    );
  }
}
ThemeTogglerButton.contextType = ThemeContext;

现在,button 除了从 context 拿 theme 以外,还会顺便拿 toggleTheme 函式。

因为功能增加的关系,把原本的 ThemeButton 改名为 ThemeTogglerButton

最後修改 App component:

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}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

App component 唯一修改的部分就是把 state 加上 toggleTheme 後一并传给 ThemeContext.Provider,这样在 Provider 内的 Consumer ThemeTogglerButton 就可以同时收到 theme 的值与 theme 的更新函式了。

读者也可以到 CodePen 查看完整范例。可以看到按下 button 时,theme context 依然可以正常切换。

如何使用多个 context

在一些需求下,一个 component 可能会要使用多个 context。

举例来说,一个 Header component 可能会同时需要使用 theme 与 user 的内容。

那实作上,要如何让一个 component 使用多个 context 呢?

技巧:使用多个 Context.Consumer

如果要在一个 component 中使用多个不同的 context 只要巢状使用 Context.Consumer 的语法即可。

举例来说:

function Page() {
  return (
    <ThemeContext.Consumer>
      {(theme) => (
        <UserContext.Consumer>
          {(user) => <Content user={user} theme={theme} />}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

要注意的是,Class.contextType 语法只能让 component 使用一个 context 而已,因此无法支援使用多个 context 的需求。

接着就让我们来看看实际应用吧!

需求

现在的需求是要做一个页面,此页面会有 Header 在最上方,且 Header 要有以下功能:

  • 显示 App 的使用者名称
  • 依照 UI theme 显示颜色

范例

实作方式如下,首先定义 App 的 context:

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

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

会有两个 context:

  • ThemeContext:代表 UI theme 的设定,预设为 dark
  • UserContext:代表使用者资讯,内容只有 name 属性而已,预设为 Guest

接着就是制作显示元件 ProfileHeader

function Profile({ user, theme }) {
  return (
    <div
      style={{
        backgroundColor: theme.background,
        color: theme.color
      }}
    >
      {user}
    </div>
  );
}

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

Profile 的职责很简单,就是根据 props 显示对应的颜色与使用者。

Header 就是负责同时使用 ThemeContextUserContext 的地方。

可以看到,因为巢状的使用 context,所以最内层的 Profile 可以同时拿到 theme 与 user 的内容,这就达到要在一个 component 中使用两个 context 的需求了。

最後是 App component:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
      signedInUser: "Henry"
    };
  }

  render() {
    const { signedInUser, theme } = this.state;

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

App 则要负责两件事:

  • 设定 context
  • 提供 Provider

因为现在的 context 有 theme 与 user,因此在 state 中指定完这两个 context 的值後,就要让这些 context 值分别放到 ThemeContext.ProviderUserContext.Provider 中,才可让更下层的 Consumer 使用 context。

读者也可以到 CodePen 上参考完整范例。

另外需要注意的是,如果有两组 context consumer 经常一起使用,则可以考虑使用 Render Props 的技巧封装成一个 component,让重用性更高。

注意事项:避免在 Provider value 中创建物件

问题原因

还记得在上一章节中提过,只要 Provider 的 value 改变了,则底下的使用此 Provider 的 Consumer component 必定全部 re-render。

因此在使用 context 时应该避免在 Provider value 中直接新物件。如果每次 render 时,value 的值都是新的物件的话,就会导致下层的所有 Consumer 在也都跟着一起 re-render。在 Consumer 数量多的时候,这会造成很大的问题。

范例如下:

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

可以看到每次执行 render 时,MyContext.Providervalue 都会是一个新的物件(就算实际上内容没有改变),而这会导致 Toolbar 里的 consumer 也随着 App 的 re-render 而重新渲染。

解法

这个问题的解法就是把 Provider value 搬到 state 中。

如此一来,就算 App 重新 render 了,state 的 instance 依然会是一样的,如此就可以避免 Provider 改值导致 Consumer 也一起 re-render 的问题了。

解法范例如下:

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

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

小结

在这个章节中,我们用透过范例学习了 context 的实际使用范例与技巧,内容包括:

  • 学习 Context 实际范例
  • 学习如何从巢状内层的 component 中触发 context 更新
  • 学习如何使用多个 context

最後也提到了使用 context 时应该避免 inline 的把值宣告在 Provider 的 value 上,否则可能产生大量Consumer 非预期 re-render 的行为。

参考资料


<<:  [Day 30] Heroku Scheduler

>>:  Day [28] Azure 认知服务-Custom Vision 建置

ASP.NET MVC 从入门到放弃(Day2) -Visual Studio 2019 专案建立

接下来讲讲後续说明会用到的专案建立方式 主控台建立 (Framework4.7.2) 1.开启Vis...

[Day15]程序菜鸟自学C++资料结构演算法 – 二元树的基本应用

前言:介绍完了二元树的建立和走访方式,紧接着要来介绍其他基本应用,一样用上一篇的程序码进行修改 可以...

谁喜欢这则贴文,初探 case...when 用法,Ruby 30 天刷题修行篇第十六话

嗨,我是 A Fei,让我们看看今天的题目: (题目来源:Codewars) You probabl...

找LeetCode上简单的题目来撑过30天啦(DAY14)

医生说我很健康真是太好了呢,今日题目如下 **题号:2 标题:Add Two Numbers 难度:...

TCP/IP vs OSI,网际网路中的协议模型

接下来的几篇,我们来看看网路中的协议到底规范了哪些东西,为什麽要有这些规则?又有何优缺点? 首先来看...