深入 React Context

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

您可能最近想知道关于 Context 的所有炒作,以及它可能对您和您的 React 网站意味着什么。在 Context 之前,当状态管理变得比 setState 的功能复杂时,您可能需要使用第三方库。感谢 React 团队的最新更新,我们现在有了 Context,它可能有助于解决一些状态管理问题。

Context 解决什么问题?

如何将状态从父组件移动到嵌套在组件树中的子组件?您知道可以使用 Redux 来管理状态,但您不必在每种情况下都跳到 Redux。

有一种方法可以在没有 Redux 或任何其他第三方状态管理工具的情况下做到这一点。您可以使用 props!

假设您要实现的功能具有类似于我下面所示的树状结构

状态存在于 App 组件中,并且在 UserProfileUserDetails 组件中需要。您需要通过 props 将其传递到树中。如果需要此状态的组件深度嵌套 10 层,这可能会变得乏味、令人疲惫且容易出错。每个组件都应该像一个黑盒——其他组件不应该知道它们不需要的状态。这是一个与上述场景相匹配的应用程序示例。

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <User user={this.state.user} />
      </div>
    )
  }
}

const User = (props) => (
  <div>
    <UserProfile {...props.user} />
  </div>
)

const UserProfile = (props) => (
  <div>
    <h2>Profile Page of {props.username}</h2>
    <UserDetails {...props} />
  </div>
)

const UserDetails = (props) => (
  <div>
    <p>Username: {props.username}</p>
    <p>First Name: {props.firstName}</p>
    <p>Last Name: {props.lastName}</p>
  </div>
)

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

我们正在使用 props 将状态从一个组件传递到另一个组件。User 组件不需要状态,但它必须通过 props 接收它才能向下传递到树中。这正是我们想要避免的。

查看 CodePen 上 Kingsley Silas Chijioke (@kinsomicrote) 编写的 React Context API Pen 1

Context 来救援!

React 的 Context API 允许您将状态存储在类似于应用程序全局状态的位置,并且仅在需要它的组件中访问它,而无需通过 props 传递它。

我们首先使用 React 的 createContext() 初始化一个新的 Context

const UserContext = React.createContext({})
const UserProvider = UserContext.Provider
const UserConsumer = UserContext.Consumer

此新 Context 被分配给一个 const 变量,在本例中,变量为 UserContext。您可以看到,现在 createContext() 在 React(16.3.0 及更高版本)中可用,因此无需安装库。

Provider 组件使上下文可用于需要它的组件,这些组件称为订阅者。换句话说,Provider 组件允许消费者订阅上下文的更改。请记住,上下文类似于应用程序的全局状态。因此,不是消费者的组件将不会订阅上下文。

如果您在本地编码,您的上下文文件将如下所示

import { createContext } from 'react'

const UserContext = createContext({})
export const UserProvider = UserContext.Provider
export const UserConsumer = UserContext.Consumer

Provider

我们将在父组件中使用 Provider,我们在其中拥有我们的状态。

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <UserProvider value={this.state.user}>
          <User />
        </UserProvider>
      </div>
    )
  }
}

Provider 接受一个 value 属性,将其传递给它的 Consumer 组件的后代。在本例中,我们将用户状态传递给 Consumer 组件。您可以看到我们没有将状态作为 props 传递给 User 组件。这意味着我们可以编辑 User 组件并排除 props,因为它不需要它们

const User = () => (
  <div>
    <UserProfile />
  </div>
)

Consumer

多个组件可以订阅一个 Provider 组件。我们的 UserProfile 组件需要使用上下文,因此它将订阅它。

const UserProfile = (props) => (
  <UserConsumer>
    {context => {
      return(
        <div>
          <h2>Profile Page of {context.username}</h2>
          <UserDetails />
        </div>
      )
    }}
  </UserConsumer>
)

我们通过 value 属性注入到 Provider 中的数据随后在函数的 context 参数中可用。我们现在可以使用它访问组件中用户的用户名。

UserDetails 组件将类似于 UserProfile 组件,因为它订阅了相同的 Provider

const UserDetails = () => (
  <div>
    <UserConsumer>
      {context => {
        return (
          <div>
            <p>Userame: {context.username}</p>
            <p>First Name: {context.firstName}</p>
            <p>Last Name: {context.lastName}</p>
          </div>
        )
      }}
    </UserConsumer>
  </div>
)

查看 CodePen 上 Kingsley Silas Chijioke (@kinsomicrote) 编写的 React Context API Pen 2

更新状态

如果我们想允许用户更改他们的名字和姓氏呢?这也是可能的。Consumer 组件可以在 Provider 组件传递的值发生更改时重新渲染。让我们看一个例子。

我们在 Consumer 组件中将有两个输入字段用于名字和姓氏。从 Provider 组件中,我们将有两个方法使用输入字段中输入的值更新应用程序的状态。少说多做,让我们开始编码!

我们的 App 组件将如下所示

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  } 

  render() {
    return(
      <div>
        <UserProvider value={
          {
            state: this.state.user,
            actions: {
              handleFirstNameChange: event => {
                const value = event.target.value
                this.setState(prevState => ({
                  user: {
                    ...prevState.user,
                    firstName: value
                  }
                }))
              },

              handleLastNameChange: event => {
                const value = event.target.value
                this.setState(prevState => ({
                  user: {
                    ...prevState.user,
                    lastName: value
                  }
                }))
              }
            }
          }
        }>
          <User />
        </UserProvider>
      </div>
    )
  }
}

我们将包含状态和操作的对象传递给 Provider 接收的 value 属性。操作是在发生 onChange 事件时将触发的函数。然后使用事件的值来更新状态。由于我们希望更新名字或姓氏,因此需要保留另一个的值。为此,我们使用了 ES6 展开运算符,它允许我们更新指定键的值。

有了新的更改,我们需要更新 UserProfile 组件。

const UserProfile = (props) => (
  <UserConsumer>
    {({state}) => {
      return(
        <div>
          <h2>Profile Page of {state.username}</h2>
          <UserDetails />
        </div>
      )
    }}
  </UserConsumer>
)

我们使用 ES6 解构 从 Provider 接收的值中提取状态。

对于 UserDetails 组件,我们同时使用状态和操作。我们还需要添加两个输入字段,这些字段将侦听 onChange() 事件并调用相应的方法。

const UserDetails = () => {
  return (
    <div>
      <UserConsumer>
        {({ state, actions }) => {
          return (
            <div>
              <div>
                <p>Userame: {state.username}</p>
                <p>First Name: {state.firstName}</p>
                <p>Last Name: {state.lastName}</p>
              </div>
              <div>
                <div>
                  <input type="text" value={state.firstName} onChange={actions.handleFirstNameChange} />
                </div>
                <div>
                  <input type="text" value={state.lastName} onChange={actions.handleLastNameChange} />
                </div>
              </div>
            </div>
          )
        }}
      </UserConsumer>
    </div>
  )
}

使用默认值

在初始化 Context 时可以传递默认值。为此,我们将传递一些数据,而不是将空对象传递给 createContext()

const UserContext = React.createContext({
  username: 'johndoe',
  firstName: 'John',
  lastName: 'Doe'
})

为了在我们的应用程序树中使用此数据,我们必须从树中删除 Provider。因此,我们的 App 组件将如下所示。

class App extends React.Component {
  state = {
    user: {
      username: 'jioke',
      firstName: 'Kingsley',
      lastName: 'Silas'
    }
  }

  render() {
    return(
      <div>
        <User />
      </div>
    )
  }
}

查看 CodePen 上 Kingsley Silas Chijioke (@kinsomicrote) 编写的 React Context API Pen 4

将在 Consumer 组件中使用的数据将在我们初始化新 Context 时定义。

总结

当事情变得复杂,并且您想运行 yarn install [<插入用于状态管理的第三方库] 时,请暂停一下——您已经准备好了 React Context。您不相信我?也许你会相信 Kent C. Dodds