初探React Context API
最近在整理CSS自定义属性在React中的使用时了解到“可以使用React Context API相关的知识更好的在React组件中使用CSS自定义属性”,但是自己对这方面的知识了解的并不多,因此想借此机会来学习React Context API相关的知识。也基于这个原因有了这篇文章。
为什么要React Context API
我们从一个React的实例开始。假设你要构建一个React的应用,该应用有一个最简单的功能,就是Dark Mode的切换。简单地说,在Web应用上一个切换组件(比如ThemeToggle
),用户点击该切换按钮可以让页面在暗色系(dark
)和亮色系(light
)之间切换。
通常我们会通过props
为所有组件提供当前主题的模式,并使用state
来更新当前的主题。
在/src/components/
目录下分别创建了GrandChild
、Child
、ParentComponent
和ThemeToggle
几个组件:
示例代码如下:
// /src/components/GrandChild
import React from 'react'
const GrandChild = (props) => {
const styled = {
color: `${props.theme.color}`,
background: `${props.theme.background}`
}
return <h1 style={{...styled,...props.styles}}>Theme Toggle</h1>
}
export default GrandChild;
// /src/components/Child
import React from 'react'
import GrandChild from '../GrandChild'
const Child = (props) => {
const styled = {
border: `5px solid ${props.theme.color}`,
padding: `10vmin 20vmin`,
borderRadius: '8px'
}
return <GrandChild theme = {props.theme} styles={styled} />
}
export default Child
// /src/components/ParentComponent
import React from 'react'
import Child from '../Child'
const ParentComponent = (props) => <Child theme = {props.theme} />
export default ParentComponent
// /src/components/ThemeToggle
import React from 'react'
const ThemeToggle = (props) => {
const styled = {
background: `${props.theme.background}`,
color: `${props.theme.color}`,
border: `4px solid currentColor`,
borderRadius: `6px`,
padding: `2vmin 4vmin`,
margin: `4vmin`,
cursor: `pointer`
}
return <button onClick={props.click} style={styled}>Toggle Dark Mode</button>
}
export default ThemeToggle
// /src/App.js
import React, {Fragment} from 'react';
import ParentComponent from './components/ParentComponent'
import ThemeToggle from './components/ThemeToggle'
const dark = {
background: '#121212',
color: '#fff'
}
const light = {
background: '#fff',
color: '#444'
}
const App = () => {
const [theme, setTheme] = React.useState('light')
const onClickHander = () => {
theme === 'light' ? setTheme('dark') : setTheme('light')
}
return <Fragment>
<ParentComponent theme={theme === 'light' ? light : dark} />
<ThemeToggle click={onClickHander} theme={theme === 'light' ? light : dark} />
</Fragment>
}
export default App;
效果如下:
在这个示例中,在ParentComponent
组件中指定了theme
这个props
,并且将这个props
一级一级往下传,传给组件树下的所有组件。即,将theme
传递到需要它的地方,在本例中会传到GrandChild
组件。而Child
组件和theme
(props
)没有任何关系,它只是作为一个媒体而以。
试想一下,在React中组件树就有点类似于我们熟悉的DOM树:
注意,上图中每个白色的矩形方框代表的就是React的组件。
正如上图所示,我们可以在最底层组件中添加state
,但如果要将数据传递给兄弟组件的话,在React Context API之前,我们只能将state
放到他们的父组件中(组件树中更高的组件位置),然后通过props
将其传递回同级组件:
就像上面的示例,我们需要将state
从组件树的最顶层一级一级往下传,哪怕是所有中间层组件不需要使用这些数据,但它必须为了后面的组件做为媒介,将state
传递到最底层组件。
在React社区中,将这种冗长和耗时的过程称为Prop Drilling。
对应到上面的示例中,那就是:
React Context API正是用来解决Prop Drilling的问题。React Context API提供了一种通过Provider
和Consumer
用来提供数据和消费数据,它们可以在组件件中传递数据,最主要的是不必要一级一级的通过props
向组件树传递state
。简单地说,在组件树最顶层的组件中通过Provider
提供数据,在后面的任何一个子组件树可以通过Consumer
来消费Provider
提供的数据:
React Context API简介
Context提供了一个无需为每层组件手动添加
props
,就能在组件树间进行数据传递的方法。
在这个特性还没出现之前,在React应用中数据的通讯是通过props
属性自上而下(由父及子)进行传递的,换句话说,必须通过props
传递到每个组件中,然后在组件中重复相同的过程。但这种做法对于某些类型的属性而言是极其繁锁的,也会变得非常的糟糕,最终可能会导致props
在我们的组件中要不断的一层一层嵌套。
React Context API的出现主要是为了帮助我们解决这方面的问题,它提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props
。也就是说,它允许父组件隐式地将数据传递给子组件(不管组件树有多深)。换句话说,可以将数据添加到父组件中,然后任何子组件都可以访问它。
假设我们有这样的一个使用场景,在一个React应用中,我们有App
、Container
、Form
和Button
四个组件,它们之间是依次被嵌套:
App ➜ Container ➜ Form ➜ Buttton
假如我们使用props
传递就需要一层一层往里传:
换成Context
,就可以直接获取最顶层App
组件绑定的值:
React Context API入门级应用
对于像我这样的初级使用者而言,要想彻底的了解React Context API,只能从最简单的应用开始。为了更好的理解它的使用,我们从最简单的示例开始。
创建上下文对象
在JavaScript中(或React)中常将Context称为上下文。
在React中使用Context的话,我们首要做的就是创建上下文对象(Context Object)。可以使用React
上的.createContext()
创建一个Context
对象:
const DataContext = React.createContext()
尝试着把这个Context
对象DataContext
在控制台上打印出来:
这个时候可以从组件树中离自身最近的那个匹配的Prrovider
中读取到当前的context
值。
只有当组件所处的树中没有匹配到Provider
时,其defaultValue
参数才会生效。这有助于在不使用Provider
包装组件的情况下对组件进地测试。
另外,createContext()
方法提供了Provider
和Consumer
能力,其中一个是提供者,另一个是消费者,而且这两个属性都是成对出现的,即每一个Provider
都会有对应的一个Consumer
。
Provider
将作为父组件使用,它持有所有Consumer
都可以共享的值。注意,Consumer
只能用于Provider
的子组件。
使用Provider
提供数据
上一步,使用React.createContext()
创建了一个名为DataContext
的上下文对象,在其中,我们用一些值(value
)初始化一个状态(state
),可以使用DataContext
的Provider
接受一个value
属性,传递给子组件消费(Consumer
)
const App = () => <DataContext.Provider value={{userName: 'Airen', age: 30 }}>
<h4>Child Component</h4>
</DataContext.Provider>;
Provider
的value
属性的值可以是字符串、数字或对象。
消费Provider
提供的数据
Provider
创建了数据,其创建的数据可以通过context
对象的Consumer
属性给子组件消费。主要有三种方法来消费Provider
属性创建的数据。
使用Consumer
组件消费数据
创建一个新组件,并且在该组件中使用DataContext
的Consumer
来消费数据。这将返回一个函数,该函数允许组件消费Provider
中设置的值。比如:
const ParagraphChildComponent = () => <DataContext.Consumer>
{ value => <h4>I'm {value.userName}, {value.age} yeas old this year!</h4>}
</DataContext.Consumer>
const App = () => <DataContext.Provider value={{userName: 'Airen', age: 30 }}>
<ParagraphChildComponent />
</DataContext.Provider>;
浏览器渲染出来的结果如下:
注意,DataContext.Consumer
组件中返回的函数是基于context
值进行渲染。
这种方式对于处理不需要任何逻辑的组件非常有用。
使用this.context
和contextType
来消费数据
在使用类的组件中,可以使用this.context
和conteextType
来消费Provider
组件中设置的值。比如:
class ParagraphChildComponentWithClass extends React.Component {
render() {
return <h4>He is {this.context.userName}, {this.context.age} yeas old this year!</h4>
}
}
ParagraphChildComponentWithClass.contextType = DataContext
const App = () => <DataContext.Provider value={{userName: 'Airen', age: 30 }}>
<ParagraphChildComponentWithClass />
</DataContext.Provider>;
挂载在class
上的contextType
属性会被重新赋值为一个由React.createContext()
创建的context
对象。这能让你使用this.context
来消费最近context
上的那个值。你可以在任何生命周期中访问到它,包括render
函数中。
class MyClass extends React.Component {
componentDidMount() {
let value = this.context
console.log('➜componentDidMount➜➜➜', value)
}
componentDidUpdate() {
let value = this.context
console.log('➜componentDidUpdate➜➜➜', value)
}
componentWillUnmount(){
let value = this.context
console.log('➜componentWillUnmount➜➜➜', value)
}
render() {
let value = this.context
return <h4>➜UserName: {value.userName}➜➜➜ Age: {value.age}</h4>
}
}
MyClass.contextType = DataContext
const App = () => <DataContext.Provider value={{userName: 'Airen', age: 30 }}>
<MyClass />
</DataContext.Provider>;
使用useContext
来消费数据
上面两种方式都可以用来消费Provider
提供的数据,但这两种方法要在适用的组件中使用。除此之外,还可以使用React的useContext
方法来消费Provider
提供的数据。该方法用于从context
中获取props
,其方法与上面两种方法相同,但看起来更简单得多:
const ParagraphChildComponentWithUseContext = () => {
const {userName, age} = useContext(DataContext)
return <h4>She is {userName}, {age} yeas old this year!</h4>
}
const App = () => <DataContext.Provider value={{userName: 'Airen', age: 30 }}>
<ParagraphChildComponentWithUseContext />
</DataContext.Provider>;
后面我们会多花一点时间来聊
useContext
。
现在对React Context API有了一个最基础的了解,接下来我们来看看在实例中怎么使用它。
React Context API实例
我们来使用React Context API改造文章最开始的Dark Mode案例。
首先需要创建一个React的上下文context
,正如上面介绍的,可以使用React.createContext
来创建。比如,我在src/
目录下创建一个ThemeContext.js
文件,用来管理这个context
。在这个示例中,给.createContext()
传入一个字符串light
,它是当前的主题模式:
// src/ThemeContext.js
import React from 'react'
const ThemeContext = React.createContext('light')
export default ThemeContext;
为了使ThemeContext
这个上下文对所有React组件可用,我们必须在React组件树的最顶层,使用Provider
来为后代子组件提供需要消费的数据。因为我们需要在light
和dark
两种状态下切换,因此,需要对ThemeContext.js
稍作修改。使用React.createContext()
创建context
上下文时需要接受一个类似useState
钩子结果的对象作为参数:
// src/ThemeContext.js
import React from 'react'
const ThemeContext = React.createContext(['light', () => {}])
export default ThemeContext;
在这个示例中,我们在App.js
文件中来创建上下文的Provider
。在创建Provider
之前,需要先导入上面已创建的ThemeContext
:
// src/App.js
import ThemeContext from './ThemeContext'
一旦ThemeContext
被导入,那么App
组件中的内容必须包含在ThemeContext.Provider
标签内。Provider
组件有一个名为value
的props
,它将包含我们想要给组件树中组件所需要的数据。这个时候,我们的App
组件类似下面这样:
// src/App.js
import React from 'react';
import ThemeContext from './ThemeContext'
const App = () => {
const theme = 'light'
return <ThemeContext.Provider value={theme}>
<div>子组件放在这里</div>
</ThemeContext.Provider>
}
export default App;
这个时候,light
的值可以用于React组件树中的所有组件。
现在我们可以像前面一样。创建Dark Mode所需要的颜色体系。
const dark = {
background: '#121212',
color: '#fff'
}
const light = {
background: '#fff',
color: '#444'
}
为好更好管理这两个主题颜色,这里将它们放到一个名为Color.js
文件中(该文件也放在src/
目录下):
// src/Color.js
export const dark = {
background: '#121212',
color: '#fff'
}
export const light = {
background: '#fff',
color: '#444'
}
接下来,我们来改造前面创建的ParentComponent
、Child
、GrandChild
和ThemeToggle
组件。先来重写ThemeToggle
组件:
// src/components/ThemeToggle/index.js
import React, { useContext } from 'react'
import { dark, light} from '../../Color'
import ThemeContext from '../../ThemeContext'
const ThemeToggle = () => {
const [theme, setTheme] = useContext(ThemeContext)
const clickHandler = () => {setTheme(theme === 'light' ? 'dark' : 'light')}
const currentTheme = theme === 'light' ? light : dark
const styledButton = {
background: `${currentTheme.background}`,
color: `${currentTheme.color}`,
border: `4px solid currentColor`,
padding: `2vmin 4vmin`,
margin: `4vmin`,
cursor: `pointer`,
borderRadius: `6px`,
fontSize: `2rem`,
fontFamily: `"Gochi Hand", sans-serif`
}
return <button style={styledButton} onClick={clickHandler}>{theme === 'light' ? 'Toggle Dark' : 'Toggle Light'}</button>
}
export default ThemeToggle
这里我们采用了useContext
来消费顶层组件App
的ThemeContext.Provider
提供的数据,它将返回一个数组。我们使用ES6的解构(Destructuring),可以从数组中获取元素。然后,为ThemeToggle
组件写一个onClick
事件处理程序,即 clickHandler
。
接下来调整最里面的组件GrandChild
:
// src/components/GrandChild/index.js
import React, { useContext } from 'react'
import ThemeContext from '../../ThemeContext'
import {dark, light} from '../../Color'
const GrandChild = () => {
const theme = useContext(ThemeContext)[0]
const currentTheme = theme === 'light' ? light : dark
const styled = {
color: `${currentTheme.color}`,
background: `${currentTheme.background}`,
border: `5px solid ${currentTheme.color}`,
padding: `10vmin 20vmin`,
borderRadius: '8px'
}
return <h1 style={styled}>{ theme === 'light' ? 'Light' : 'Dark'} Theme</h1>
}
export default GrandChild;
在这里使用useContext(ThemeContext)[0]
取出context
上下文中数组中的第一个值,即theme
。然后根据theme
值的变化获取Color.js
对应的两个颜色主题dark
和light
。这样就可以将对应主题的颜色运用到元素中。
对于Child
和ParentComponent
两个组件,我们要变得容易地多:
// src/components/ParentComponent/index.js
import React from 'react'
import Child from '../Child'
const ParentComponent = () => <Child />
export default ParentComponent
// src/components/Child/index.js
import React from 'react'
import GrandChild from '../GrandChild'
const Child = () => <GrandChild />
export default Child
最后的效果如下:
虽然GrandChild
组件是组件树中最底层,但通过useContext()
可以直接消费最顶层提供的数据:
React Context API来管理全局状态
在React中,以往都是使用Redux工具来管理全局状态,但随着React Context API的出现,我们可以使用它来创建一个类似于Redux的全局状态管理工具。
React的官方文档也提到过,React Context API提供了一种通过组件树传递数据的方式,而不履在每一层手动传递props
。事实上,在React中我们可以结合useContext
笔useReducer
两个钩子函数,创建一个全局存储(store
)来管理整个应用程序的状态,并支持在整个应用程序中方便地更新状态(state
)。
为了更好的向大家阐述这方面的特性,即,使用React Context API和React的useContext
、useReducer
钩子函数来管理React应用程序的全局状态。用一个计数器的示例来帮助我们理解这方面的特性。
创建一个存储器
首先在src/
目录下创建一个store.js
文件,用来创建全局状态的存储器。 这个存储器(该文件)包含两个部分:context
(上下文)和 state
(状态)。使用React.createContext()
创建一个React上下文,会返回一个Provider
,它会接受一个value
值(即props
)。Provider
将会包裹整个应用程序(App
)。而且value
将接收useReducer
钩子函数创建的状态(state
)。该钩子函数接受一个(state, action) => newState
类型的Reducer
,并返回新的状态(newState
)和一个dispatch
函数。Reducer
(减速器)定义了一组操作(action
)以及根据操作类型更新状态的方式。
useContext
钩子是组件访问全局存储(Global Store)的方式。最后,创建一个自定义钩子useStore
,这样就不必要导出StoreContext
,然后导入它和useContext
。
// src/store.js
import React, { createContext, useReducer, useContext } from 'react'
const StoreContext = createContext()
const initialState = {
count: 0,
message: ''
}
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {
count: state.count + 1,
message: action.message
}
case 'decrement':
return {
count: state.count - 1,
message: action.message
}
case 'reset':
return {
count: 0,
message: action.message
}
default:
throw new Error(`未定义的操作类似:${action.type}`)
}
}
export const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return <StoreContext.Provider value={{state, dispatch}}>
{children}
</StoreContext.Provider>
}
export const useStore = () => useContext(StoreContext)
让全局存储可以随处被访问
为了让store
可以随处可访问,将整个应用程序封装在刚刚创建的Provider
中。
// src/App.js
import React from 'react';
import {StoreProvider} from '../src/store'
import './App.css';
const App = () => {
return <StoreProvider>
<h4>所有子组件放在这里</h4>
</StoreProvider>
}
export default App;
这样我们就要以在任何地方访问useStore
。比如新创建的Count
组件中,从store
中提取state
和dispatch
函数。dispatch
需要调用action
类型以及任何有效的负载(Payload)信息,比如本例中的message
。
// src/components/Count
import React from 'react'
import { useStore } from '../../store'
import './index.css'
const Count = () => {
const {state, dispatch} = useStore()
const incrementClickHandler = () => dispatch({
type: 'increment',
message: 'Incremented'
})
const decrementClickHandler = () => dispatch({
type: 'decrement',
message: 'Decremented'
})
const resetClickHandler = () => dispatch({
type: 'reset',
message: 'Reset'
})
return <div className="count">
<h1>{state.message} {state.count} </h1>
<div className="actions">
<button onClick={incrementClickHandler} className="button increment"><span>+</span></button>
<button onClick={decrementClickHandler} className="button decrement"><span>-</span></button>
<button onClick={resetClickHandler} className="button reset"><span>R</span></button>
</div>
</div>
}
export default Count
在App
的<StoreProvider>
中引用Count
组件:
const App = () => {
return <StoreProvider>
<Count />
</StoreProvider>
}
看到的效果如下:
虽然示例中提到的这种方式,我们可以很好的管理全局的状态,但也存在一定的缺陷:
每当上下文(
context
)被修改时,useContext
会重新渲染。
也就是说,订阅全局状态的每个组件将在上下文更新时重新渲染,对页面的性能有直接影响。
React Context API对性能的影响
上面的示例提到过,订阅全局状态的每个组件将在上下文更新时会重新渲染。那么我们要如何避免这方面的问题呢。我们从一个简单的示例开始,一步一步地向大家演示在使用React Context API和React Hooks时在哪些方面要改进,以及组件如何避免上下文更新时重新渲染。
首先在src/
目录下创建一个名为GlobalContext.js
文件,主要用来存放创建Context
所需的逻辑:
// src/GlobalContext.js
import React, { createContext } from 'react'
const GlobalContext = createContext()
export default GlobalContext
创建了一个最简单的上下文GlobalContext
,这个时候,我们就可以在App.js
中使用它:
// src/App.js
import React from 'react';
import GlobalContext from '../src/GlobalContext'
import './App.css';
const App = () => {
return <GlobalContext.Provider>
<h1>子组件放这里</h1>
</GlobalContext.Provider>
}
export default App;
接着创建一个ProfileCard
组件,当它被挂载(mounted
)时,会通过调用API得到用户个人信息,然后使用setProfile
更新状态并显示它们。
// src/components/ProfileCard
import React, { useState, useEffect } from 'react'
const ProfileCard = props => {
const [profile, setProfile] = useState([])
useEffect(() => {
(async () => {
const result = await fetch('https://api.github.com/users/airen')
const data = await result.json()
setProfile(data)
})()
},[])
const { avatar_url,location, name, bio, public_repos, followers, following, html_url } = profile
return <div className="profile">
<div className="header">
<div className="avatar">
<a href={html_url}>
<img src={avatar_url} alt={name} />
</a>
</div>
<h2>{name} <span>{bio}</span></h2>
<h3>{location}</h3>
<div className="footer">
<ul className="details">
<li>
<span>{followers}</span>
<strong>Followers</strong>
</li>
<li>
<span>{following}</span>
<strong>Following</strong>
</li>
</ul>
</div>
</div>
</div>
}
export default ProfileCard
并将ProfileCard
组件放到<GlobalContext.Provider>
标签内:
// src/App.js
const App = () => {
return <GlobalContext.Provider>
<ProfileCard />
</GlobalContext.Provider>
}
加上CSS之后,看到的效果如下:
我们请求数据需要有一定的时间,我们创建一个Loading
组件,这样在加载数据的时候,可以告诉用户:
// src/components/Loading
import React from 'react'
import './index.css'
const Loading = props => {
return <div className="global-spinner-overlay">
<ul className="loading-animation alternate">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
}
export default Loading
同样将该组件引入到App
中:
// src/App.js
const App = () => {
return <GlobalContext.Provider>
<Loading />
<ProfileCard />
</GlobalContext.Provider>
}
现在的效果并不完美。我们还必须在App.js
为Loading
组件添加一些逻辑,控制它什么时候显示。接下来,创建一个ContextProvider
组件,它将封装这个逻辑,并且让App.js
更干净。先对前面的GlobalContext.js
做修改,不再导出GlobalContext
,而是将它作为一个常量。另外在该文件中创建一个GlobalContextProvider
组件,将使用useState
钩子一米存储和更新Loading
组件可见性状态(state
)。为了达到该效果,可能会像下面这样做:
// src/GlobalContext.js
import React, { createContext, useState } from 'react'
export const GlobalContext = createContext()
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
return <GlobalContext.Provider value={{isLoadingOn, setLoading}}>
{props.children}
</GlobalContext.Provider>
}
export default GlobalContextProvider
同时App.js
中的代码也需要做相应的调整:
// src/App.js
const App = () => {
return <GlobalContextProvider>
<Loading />
<ProfileCard />
</GlobalContextProvider>
}
然后在Loading
组件中,导入GlobalContext
而且使用useContext
钩子函数引入Provider
的isLoadingOn
来控制Loading
组件的渲染。当isLoadingOn
为true
时显示Loading
,反之不显示:
// src/components/Loading
import React, { useContext } from 'react'
import {GlobalContext} from '../../GlobalContext'
import './index.css'
const Loading = props => {
const {isLoadingOn} = useContext(GlobalContext)
return isLoadingOn ? <div className="global-spinner-overlay">
<ul className="loading-animation alternate">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div> : null
}
export default Loading
按同样的方式,在ProfileCard
中引入GlobalContext
,但不需要isLoadingOn
,而是需要setLoading
函数:
// src/components/ProfileCard
import React, { useState, useEffect, useContext } from 'react'
import {GlobalContext} from '../../GlobalContext'
import './index.css'
const ProfileCard = props => {
const [profile, setProfile] = useState([])
const { setLoading } = useContext(GlobalContext)
useEffect(() => {
(async () => {
setLoading(true)
const result = await fetch('https://api.github.com/users/airen')
const data = await result.json()
setProfile(data)
setLoading(false)
})()
},[setLoading])
const { avatar_url,location, name, bio, followers, following, html_url } = profile
return <div className="profile">
<div className="header">
<div className="avatar">
<a href={html_url}>
<img src={avatar_url} alt={name} />
</a>
</div>
<h2>{name} <span>{bio}</span></h2>
<h3>{location}</h3>
<div className="footer">
<ul className="details">
<li>
<span>{followers}</span>
<strong>Followers</strong>
</li>
<li>
<span>{following}</span>
<strong>Following</strong>
</li>
</ul>
</div>
</div>
</div>
}
export default ProfileCard
上面演示的是最简单的实现,但它存在一些问题。我们继续在该示例的基础上进行优化。首先需要对GlobalContext
进行改造。就是实现如何将isLoadingOn
和setLoading
传递给上下文的Provider
。
<GlobalContext.Provider value={{isGlobalOn, setLoading}}>
{props.children}
</GlobalContext.Provider>
当传递给Provider
的值发生更改时,会重新渲染所有上下文使用者,这也意思味着,如果改变isLoadingOn
的值或父组件重新渲染,那么Loading
和ProfileCard
组件都将重新渲染。这对于页面的性能来说是有很大影响。要解决这个问题,可以使用useMome
钩子函数,它会缓存值对象。这样一来,只有isLoadingOn
的值发生变化时,它才会被重新创建。
// src/GlobalContext.js
import React, { createContext, useState, useMemo } from 'react'
export const GlobalContext = createContext()
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
const value = useMemo(() =>({
isLoadingOn,
setLoading
}),[isLoadingOn])
return <GlobalContext.Provider value={value}>
{props.children}
</GlobalContext.Provider>
}
export default GlobalContextProvider
这解决了在每个渲染上重新创建一个新对象,从而重新渲染所有消费者(Consumer
)的问题。不幸的是,这样还是存在一个问题。即避免重新渲染所有上下文使用者。
现在,只要isLoadingOn
值发生变化就会创建一个新的值对象。然而,尽管Loading
组件依赖于isLoadingOn
,但它并不依赖于setLoading
函数。同样,ProfileCard
只需要访问setLoading
函数。因此,在每次isLoadingOn
可见性发彺更改时都重新渲染ProfileCard
是没有意义的,因为组件并不直接依赖于它。所以说,为了避免这个问题,我们可以创建另一个上下文来分离isLoadingOn
和setLoading
。
这个时候GlobalContext.js
文件可以改造成:
// src/GlobalContext.js
import React, { createContext, useState, useMemo } from 'react'
export const GlobalContext = createContext()
export const GlobalActionsContext = createContext()
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
return <GlobalContext.Provider value={isLoadingOn}>
<GlobalActionsContext.Provider value={setLoading}>
{props.children}
</GlobalActionsContext.Provider>
</GlobalContext.Provider>
}
export default GlobalContextProvider
由于有两个上下文提供者(Provider
),组件可以准确地使用它们需要的内容。现在,我们需要更新Loading
和ProfileCard
组件时使用正确的值。
Loading
组件原本就是通地GlobalContext
来消费Provider
提供的数据,所以它不需要做任何的调整。但ProfileCard
组件中,需要将GlobalContext
更换成新创建的上下文GlobalActionsContext
:
// src/components/ProfileCard
import React, { useState, useEffect, useContext } from 'react'
import {GlobalActionsContext} from '../../GlobalContext'
import './index.css'
const ProfileCard = props => {
const [profile, setProfile] = useState([])
const setLoading = useContext(GlobalActionsContext)
useEffect(() => {
(async () => {
setLoading(true)
const result = await fetch('https://api.github.com/users/airen')
const data = await result.json()
setProfile(data)
setLoading(false)
})()
},[setLoading])
const { avatar_url,location, name, bio, followers, following, html_url } = profile
return <div className="profile">
<div className="header">
<div className="avatar">
<a href={html_url}>
<img src={avatar_url} alt={name} />
</a>
</div>
<h2>{name} <span>{bio}</span></h2>
<h3>{location}</h3>
<div className="footer">
<ul className="details">
<li>
<span>{followers}</span>
<strong>Followers</strong>
</li>
<li>
<span>{following}</span>
<strong>Following</strong>
</li>
</ul>
</div>
</div>
</div>
}
export default ProfileCard
这样一来,我们就解决了性能问题。但我们还有地方需要进一步的改进,即使用上下文值的方式有关。
要在任何组件中使用isLoadingOn
上下文值,我们必须直接导入上下文以及useContext
钩子。通过使用useContext
色子调用的包装器,我们可以使它变得不那么单调乏味。我们不再直接导出上下文值,而是使用自定义函数来使用上下文。
// src/GlobalContext.js
import React, { createContext, useState, useContext } from 'react'
const GlobalContext = createContext()
const GlobalActionsContext = createContext()
export const useGlobalContext = () => useContext(GlobalContext)
export const useGlobalActionsContext = () => useContext(GlobalActionsContext)
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
return <GlobalContext.Provider value={isLoadingOn}>
<GlobalActionsContext.Provider value={setLoading}>
{props.children}
</GlobalActionsContext.Provider>
</GlobalContext.Provider>
}
export default GlobalContextProvider
接下来,我们必须调整Loading
和ProfileCard
组件,并使用useGlobalContext
和useGlobalActionsContext
来替换useContext
钩子函数:
// src/components/Loading
import React from 'react'
import {useGlobalContext} from '../../GlobalContext'
import './index.css'
const Loading = props => {
const isLoadingOn = useGlobalContext()
return isLoadingOn ? <div className="global-spinner-overlay">
<ul className="loading-animation alternate">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div> : null
}
export default Loading
// src/components/ProfileCard
import React, { useState, useEffect } from 'react'
import {useGlobalActionsContext} from '../../GlobalContext'
import './index.css'
const ProfileCard = props => {
const [profile, setProfile] = useState([])
const setLoading = useGlobalActionsContext()
useEffect(() => {
(async () => {
setLoading(true)
const result = await fetch('https://api.github.com/users/airen')
const data = await result.json()
setProfile(data)
setLoading(false)
})()
},[setLoading])
const { avatar_url,location, name, bio, followers, following, html_url } = profile
return <div className="profile">
<div className="header">
<div className="avatar">
<a href={html_url}>
<img src={avatar_url} alt={name} />
</a>
</div>
<h2>{name} <span>{bio}</span></h2>
<h3>{location}</h3>
<div className="footer">
<ul className="details">
<li>
<span>{followers}</span>
<strong>Followers</strong>
</li>
<li>
<span>{following}</span>
<strong>Following</strong>
</li>
</ul>
</div>
</div>
</div>
}
export default ProfileCard
由于useContext
只能在Context.Provider
中调用。为了确保我们不会在Provider
之外使用上下文,我们可以检查是否有上下文值。
// src/GlobalContext.js
import React, { createContext, useState, useContext } from 'react'
const GlobalContext = createContext()
const GlobalActionsContext = createContext()
export const useGlobalContext = () => {
const context = useContext(GlobalContext)
if (context === undefined) {
throw new Error(`useGlobalContext must be called within GlobalContextProvider`)
}
return context
}
export const useGlobalActionsContext = () => {
const context = useContext(GlobalActionsContext)
if (context === undefined) {
throw new Error(`useGlobalActionsContext must be called within GlobalContextProvider`)
}
return context
}
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
return <GlobalContext.Provider value={isLoadingOn}>
<GlobalActionsContext.Provider value={setLoading}>
{props.children}
</GlobalActionsContext.Provider>
</GlobalContext.Provider>
}
export default GlobalContextProvider
正如上面代码所示,我们首先检查context
值,而不是立即返回useContext
的结果。如果context
未定义,则抛出相应错误。然而,对于每个useContext
消费者(Consumer
)都这样做有点重复,因此,我们可以将其抽取出来重复使用。
// src/GlobalContext.js
import React, { createContext, useState, useContext } from 'react'
const GlobalContext = createContext()
const GlobalActionsContext = createContext()
/* eslint-disable*/
const useContextFactory = (name, context) => {
return () => {
const ctx = useContext(context)
if (ctx === undefined) {
throw new Error(`use${name}Context must be use withing a ${name}ContextProvider`)
}
return ctx
}
}
/* eslint-enable */
export const useGlobalContext = useContextFactory('GlobalContext', GlobalContext)
export const useGlobalActionsContext = useContextFactory('GlobalActionsContext', GlobalActionsContext)
const GlobalContextProvider = (props) => {
const [isLoadingOn, setLoading] = useState(false)
return <GlobalContext.Provider value={isLoadingOn}>
<GlobalActionsContext.Provider value={setLoading}>
{props.children}
</GlobalActionsContext.Provider>
</GlobalContext.Provider>
}
export default GlobalContextProvider
有关于这方面的介绍还可以阅读《Global state with React》一文。
小结
React Context API是React的一个特性,在React中可以很好的用来处理组件之间的数据通讯,也能更易于处理状态的管理。但React Context API也不是银弹,也不是全能的,可以用于任何场景之中。在实际使用之中,我们还是需要根据具体场景做出正确的选择。在文章中我们主要对React Context API做了一些初步的探讨,学习了这方面最基础知识和运用。如果你在这方面有经验,欢迎在下面的评论分享。