使用 Context 避免深层属性传递

623 阅读3分钟

前言

最近在项目发现了一些看起来很神奇的代码,一个组件 A 的方法通过 props 传到组件 B,然后到组件 C,再到组件 D,再到组件 E,最后到组件 D, 简直是千层饼😂

Xnip2021-07-19_20-03-31.jpg

提问一番后知道了这种将属性深层传递的现象叫 Prop Drilling,本文说明如何使用 React 的 Context API 避免这种现象。

什么是 Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。这种组件共享的以及变化不频繁的状态正是 Context 用武之地。

如何使用 Context

举个例子🌰 说明

react-context-hooks.vercel.app_(iPhone 6_7_8).png

在线示例
本文代码

创建 Context

首先用 React.createContext 方法创建一个 AuthContext,表示当前应用的认证状态,创建时可以传入默认值。

// AuthContext.js

export const AuthContext = React.createContext({
  user: null,
  isAuthenticated: false,
})

然后创建 AuthContextProvider 组件,在组件中初始化状态,定义改变状态的方法,将这些数据和方法传递给 Context.Providervalue 属性 ,value 可以接收数据和方法,最后在组件中渲染子组件 children

// AuthContext.js

export class AuthContextProvider extends Component {
  state = {
    user: null,
    isAuthenticated: false,
  }

  login = (user) => {
    // ...
  }

  logout = () => {
   // ...
  }

  render() {
    return (
      <AuthContext.Provider value={(this.state, this.login, this.logout)}>
        {this.props.children}
      </AuthContext.Provider>
    )
  }
}

使用 Context

有多种方式获得 Context 中的数据

  1. 使用 Context.Comsumer
// AppHeader.js

<AuthContext.Consumer>
  {(authContext) => (
    <header>
    // ...
    </header>
  )}
</AuthContext.Consumer>
  1. 使用组件的 ContextType 属性,将 contextType 属性赋值为想要传入的 Context
// AppFooterButtons.js

class AppFooterButtons extends Component {
  static contextType = AuthContext

  render() {
    return (
      <div>
        <button onClick={this.login}>Login</button>
        <button onClick={this.context.logout}>Logout</button>
      </div>
    )
  }
}
  1. 使用 useContext

函数式组件可以使用 useContext 方法获取状态数据,可以同时获取多个状态。

// AppBody.js

const AppBoy = () => {
  const authContext = useContext(AuthContext)
  const { languages, current } = useContext(LanguageContext)

  return (
    <div>
      <h1>
        Hello, {authContext.isAuthenticated ? authContext.user.name : 'World'}
      </h1>
       <p>Currrent Lang: {languages[current].lang}</p>
    </div>
  )
}

修改 Context

  1. 对于 class 组件,使用 setState 方法更新组件的 state
// AuthContext.js 

export class AuthContextProvider extends Component {
  // ...
  
  login = (user) => {
    if (this.state.isAuthenticated) {
      return
    }
    this.setState({
      user,
      isAuthenticated: true,
    })
  }

  logout = () => {
    this.setState({
      user: null,
      isAuthenticated: false,
    })
  }

  // ...
}

  1. 函数式组件使用 useState
// LanguageContext.js

export const LanguageContextProvider = (props) => {
  // ...
  
  const [current, setCurrent] = useState(0)
  const changeLanguage = (val) => {
    setCurrent(val)
  }

  // ...
}

最终使用 Context 将应用中通用状态统一管理,无需再层层传递。

结论

本文说明什么是 Context,以及使用 Context 的多种方式。使用 Context 可以有效的避免 Prop Drilling 现象,将需要深层传递的属性和方法提取出来,达到共用的目的。应用 Context 的最佳时机是应用的一些全局性,不会频繁变化的数据。

注意事项

使用 Context 之前的考虑

Context 主要应用场景在于_很多_不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复 用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

参考

Avoid Prop Drilling with React Context
Prop Drilling
Context