Context有什么作用?
官方描述:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。如果属性只能通过props从上往下进行传递,在有些应用场景下是非常繁琐的,比如UI主题的继承,Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
解读
- 通过Context可以让组件树之间进行数据传递,并且不需要手动添加props.
何时使用Context?
Context设计的目的是为了共享那些对于一个组件树而言是全局的数据。
例如下面的例子,通过theme属性手动调整一个按钮组件的样式
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。 // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事, // 因为必须将这个值层层传递所有组件。 return ( <div> <ThemedButton theme={props.theme} /> </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } } 复制代码
- 通过props可以避免中间元素传递props
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。 // 为当前的 theme 创建一个 context(“light”为默认值)。 const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。 // 无论多深,任何组件都能读取这个值。 // 在这个例子中,我们将 “dark” 作为当前的值传递下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // 中间的组件再也不必指明往下传递 theme 了。 function Toolbar() { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // 指定 contextType 读取当前的 theme context。 // React 会往上找到最近的 theme Provider,然后使用它的值。 // 在这个例子中,当前的 theme 值为 “dark”。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } } 复制代码
使用Context之前的考虑
- 带来的问题
Context的主要应用场景是不同层级的组件需要访问一些相同的数据,这可能会使得组件的复用性变差。
- 层层传递props带来的麻烦
<Page user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <PageLayout user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <NavigationBar user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <Link href={user.permalink}> <Avatar user={user} size={avatarSize} /> </Link> 复制代码
解读
如果在最后只有 Avatar 组件真的需要 user 和 avatarSize,那么层层传递这两个 props 就显得非常冗余。而且一旦 Avatar 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。
- 一种无需Context的解决方案(将Avatar组件自身传递下去)
function Page(props) { const user = props.user; const userLink = ( <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> ); return <PageLayout userLink={userLink} />; } // 现在,我们有这样的组件: <Page user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <PageLayout userLink={...} /> // ... 渲染出 ... <NavigationBar userLink={...} /> // ... 渲染出 ... {props.userLink} 复制代码
- 上面做法带来的问题
- 使得高层组件更加复杂。
- 传递多个组件的情况
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} /> ); } 复制代码
API
React.createContext
const MyContext = React.createContext(defaultValue); 复制代码
解读
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
注意事项
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
Context.Provider
<MyContext.Provider value={/* 某个值 */}> 复制代码
解读
- 每个Context对象都会返回一个Provider组件,这个组件允许订阅context对象的变化。
- Provider接收一个value属性,传递给消费组件。
- Provider中的value值发生变化的时候,其所有的消费组件都会重新渲染。
Class.contextType
class MyClass extends React.Component { componentDidMount() { let value = this.context; /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* 基于 MyContext 组件的值进行渲染 */ } } MyClass.contextType = MyContext; 复制代码
挂载在类上的contextType属性会被重新赋值为一个有React.createContext()创建的Context对象。这个属性能够让我们使用this.context来使用最近Context上的那个值。
class MyClass extends React.Component { static contextType = MyContext; render() { let value = this.context; /* 基于这个值进行渲染工作 */ } } 复制代码
Context.Consumer
- 这个API可以让函数式组件使用Context,相当于类组件的this.context
<MyContext.Consumer> {value => /* 基于 context 值进行渲染*/} </MyContext.Consumer> 复制代码
解读
Consumer内部的函数接收当前的context值,然后返回React节点,传递给函数的value值等价于组件树上方离这个context最近的Provider提供的value值。
Context.displayName
这个属性是用来辅助Reacat DevTools来使用字符串确定context要显示的内容的。
const MyContext = React.createContext(/* some value */); MyContext.displayName = 'MyDisplayName'; <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中 <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中 复制代码
示例
动态Context
对上面的theme例子使用动态值。
theme-context.js
export const themes = { light: { foreground: '#000000', background: '#eeeeee', }, dark: { foreground: '#ffffff', background: '#222222', }, }; export const ThemeContext = React.createContext( themes.dark // 默认值 ); 复制代码
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'; // 一个使用 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() { // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值, // 而外部的组件使用默认的 theme 值 return ( <Page> <ThemeContext.Provider value={this.state.theme}> <Toolbar changeTheme={this.toggleTheme} /> </ThemeContext.Provider> <Section> <ThemedButton /> </Section> </Page> ); } } ReactDOM.render(<App />, document.root); 复制代码
在嵌套组件中更新Context
在嵌套很深的组件中更新context是很必要的,可以通过context传递函数,是的consumers组件更新context
theme-context.js
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的! export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, }); 复制代码
theme-toggle-button.js
import {ThemeContext} from './theme-context'; function ThemeTogglerButton() { // Theme Toggler 按钮不仅仅只获取 theme 值, // 它也从 context 中获取到一个 toggleTheme 函数 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 也包含了更新函数,因此它会被传递进 context provider。 this.state = { theme: themes.light, toggleTheme: this.toggleTheme, }; } render() { // 整个 state 都被传递进 provider return ( <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root); 复制代码
消费多个Context
为了确保context快速进行重渲染,React需要使每一个消费组件的context在组件树中成为一个单独的节点。
// Theme context,默认的 theme 是 “light” 值 const ThemeContext = React.createContext('light'); // 用户登录 context const UserContext = React.createContext({ name: 'Guest', }); class App extends React.Component { render() { const {signedInUser, theme} = this.props; // 提供初始 context 值的 App 组件 return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); } } function Layout() { return ( <div> <Sidebar /> <Content /> </div> ); } // 一个组件可能会消费多个 context function Content() { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); } 复制代码
注意事项
- 下面的代码,当每一次Provider重渲染时,会重渲染下面的所有消费者组件,因为value属性总是被赋值为新的对象。
class App extends React.Component { render() { return ( <MyContext.Provider value={{something: 'something'}}> <Toolbar /> </MyContext.Provider> ); } } 复制代码
解决办法
将value状态提升到父节点的state里。
class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <MyContext.Provider value={this.state.value}> <Toolbar /> </MyContext.Provider> ); } } 复制代码