React: Context API 入门
说到使用全局数据管理状态时,第一个想到的就是 Redux。但是 Redux 语法太过啰唆,React 使用者一直很不爽。所以 React 模仿 Redux 推出了 Context API。
一个例子
我们依旧从一个例子出发,现在有 3 个函数,规定:
- f1 调用 f2
- f2 调用 f3
- 有局部变量
n
,问:怎么在 f3 中拿到这个n
?
我们可能会这样想,每次我都传这个n
到下一个组件不就好了?比如:
function f1(n1) {
console.log(1, n1);
f2(n1);
}
function f2(n2) {
console.log(2, n2);
f3(n2);
}
function f3(n3) {
console.log(3, n3);
}
{
let n = 100;
f1(n);
}
是很简单,但是很不爽。
局部的全局变量
如果我们将这个n
提到全局,那么所有的函数都能访问到这个n
就能解决第三个需求了。但是这违反了n
的作用域要求。毕竟全局变量要慎用的。
但是我们可以将两者做一个结合。先给这些函数一个局部作用域,在里面定义一个store
来存放n
,如果要改变n
,那么在里面提供一个接口,让外面去改这个n
就好了。比如:
{
let context = {};
window.setContext = function (key, value) {
context[key] = value;
};
window.f1 = function f1(n1) {
f2();
};
function f2(n2) {
f3();
}
function f3(n3) {
console.log(4, context["n"]);
}
}
{
window.setContext("n", 100);
f1();
}
这个其实就是 Context API 的大致思想:我画一个圈,在这个圈子里变量可以被里面所有人访问,但是圈子之外的人不能访问。
回到组件问题
现在回到我们的组件问题,深层组件怎么很容易地去获取全局变量?考虑如下代码,要怎么在MyBox
组件里获取 App 里的 theme
呢?
function MyDiv(props) {
return <div className={怎么在这里获取 theme 呢?}>{props.children}</div>
}
function MyButton() {
return <button>Click</button>
}
function MyInput() {
return <input type="text"/>
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
render() {
return (
<div>
<button onClick={this.change}></button>
<MyDiv>
<MyButton></MyButton>
</MyDiv>
<MyDiv>
<MyInput></MyInput>
</MyDiv>
</div>
)
}
}
Context API 就能完美地解决,而且不能每都传这个 theme
使用 Context API
造泡泡
使用下面代码创建 Context,相当于先造一个泡泡(前面的创建一个局部环境)给这些要用到全局变量的组件。
const themeContext = React.createContext()
给泡泡上色
虽然在组件 App
的 state
里定义了 theme
,但是我们还要添加 Provider
的方式将这个 theme
让上面创建的局部环境能获取这个 theme
。
<themeContext.Provider value={this.state.theme}>
<div>
<button onClick={this.change}>Change theme</button>
<MyDiv>
<MyButton></MyButton>
</MyDiv>
<MyDiv>
<MyInput></MyInput>
</MyDiv>
</div>
</themeContext.Provider>
包住组件
现在只需要将组件包在上面创建的局部环境里就可以访问到了。
<themeContext.Provider value={this.state.theme}>
<button onClick={this.change}>Change theme</button>
<themeContext.Consumer>
{theme => (
<div>
<MyDiv theme={theme}>
<MyButton></MyButton>
</MyDiv>
<MyDiv theme={theme}>
<MyInput></MyInput>
</MyDiv>
</div>)}
</themeContext.Consumer>
</themeContext.Provider>
在组件 MyDiv
里可以用 props
获取。
function MyDiv(props) {
return <div className={props.theme}>{props.children}</div>
}
看到这你会不会蒙逼?前面都好理解,这个Consumer
里的是什么鬼,又箭头,又函数,又标签,不懂啊。
聪明的 React
字符串?函数?标签?
我们先从一个简单的例子开始。
function Component() {
console.log('Hello world')
return <p>hi</p>
}
function Consumer(props) {
return <div>{props.children}</div>
}
function App() {
return (
<Consumer>
Component
{Component}
<Component/>
</Consumer>
)
}
这里猜猜会变成啥。记住,我们写 React 的时候,JSX 里的标签不是 HTML 标签,而是 JS 代码。
- Component 会变成字符串,就显示 "Component" 在页面上
- {Component} 会变成函数,由于 React 的限制,不会显示
- <Component/> 则会翻译成
React.createElement('p', null)
也就是显示成<p>hi</p>
我们注意到第二种情况 {Component}
虽然不显示,但是我们可以在 App
通过 this.props.children
去获取 Component
这个函数。
标签里传数函数
再考虑如下例子。
function Component(msg) {
console.log(msg)
return <p>hi</p>
}
function Consumer(props) {
let msg = 'Hello world'
let fakeHtml = props.children(msg)
return fakeHtml
}
function App() {
return ( <Consumer>{Component}</Consumer>)
}
- 先看组件 App 里面 组件 Consumer 里有一个
{Component}
- 再看组件 Consumer 里,我们可以用
this.props.children
去获取Component
这个函数 - 然后就执行这个函数,同时传入
msg
,在Component
函数里打印出来,并返回Component
里返回的“标签”
看到这里有没有启发呢?其实 themeContext.Consumer
做的就是这样的事,也就是:
- 在
themeContext.Consumer
里先用props.children
去获取标签里的箭头函数 - 然后将
theme
作为这个箭头函数的第一参数传进去 - 箭头函数返回的结果的 JSX “标签”(注意:这不是标签是JS代码)就用上了箭头函数传入的
theme
改值
上面很容易获取值,那么修改值呢?
App 上改值
上面的例子就在组件 App 里直接改值,加个函数 change
就好了,然后在对应的按钮绑定:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
change = () => {
this.setState({
theme: 'dark'
})
}
render() {
return (
<themeContext.Provider value={this.state.theme}>
<button onClick={this.change}>Change theme</button>
<themeContext.Consumer>
{theme => (
<div>
<MyDiv theme={theme}>
<MyButton></MyButton>
</MyDiv>
<MyDiv theme={theme}>
<MyInput></MyInput>
</MyDiv>
</div>)}
</themeContext.Consumer>
</themeContext.Provider>
)
}
}
在别的组件里改值
这个也很容易,我们可以不传 theme,可以在 value 里传入一个对象如:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
store: {
theme: 'light',
setTheme: () => {
this.setState({
store: {
...this.state.store,
theme: 'dark'
}
})
}
}
}
}
render() {
return (
<themeContext.Provider value={this.state.theme}>
<button>Change theme</button>
<themeContext.Consumer>
{store => (
<div>
<MyDiv theme={store.theme}>
<MyButton onClick={store.setTheme}></MyButton>
</MyDiv>
<MyDiv theme={store.theme}>
<MyInput></MyInput>
</MyDiv>
</div>)}
</themeContext.Consumer>
</themeContext.Provider>
)
}
}
总结
- 使用
React.createContext()
来创建 Context - 使用
<XXXContext.Provider value={obj}>...</XXXContext.Provider>
传值 - 使用
<XXXContext.Consumer>{(obj) => <F1 myValue={obj.xx}/>}</XXXContext.Consumer>
包住组件 - obj 里可以有 store, setStore,能读又能取
- 思想:使用局部的全局变量,只能在
<XXXContext.Consumer/>
里使用。要用变量的时候就加一个局部环境给那些要用的组件,这样也不会影响别的组件。
有没有感觉这个比 Redux 舒服很多呢?没有一大堆的概念和摸不着头脑的代码分离,只有简单的传值。
(完)