依照3W学习原则的模式出发来思考Context的使用问题,在学会如何使用Context之前,首先我们需要来思考一下几个问题:
使用之前较为清晰的了解使用研究对象更加有助于我们的认知和加深我们对Context的理解程度。
Context是什么?
Context(上下文)一直在各种语言、平台之中占据较为重要的地位,它是整个程序的链接者,一直贯穿应用的整个生命周期。React中的Context也类似,Context旨在为React复杂嵌套的各个组件提供一个生命周期内的统一属性访问对象,从而避免我们出现当出现复杂嵌套结构的组件需要一层层通过属性传递值得问题。
其设计之初的构想就是为了提供一个组件树形结构内的一个数据共享的容器。
为什么需要用Context?
为何要用Context,前文也有所介绍,其主要原因还是由于不使用Context无法完成某些业务场景或者是实现起来很难。我们可以通过两张图来了解下在是否使用Context场景下,其对应的属性值传递的过程。来帮助我们了解为什么Context非用不可。
- 不使用Context
传统方式不使用Context,数据的传递如上图属于嵌套关系,只能通过有父子包含关系的组件一层层进行传递,数据传递的层级随着结构的增加而成倍增加。
- 使用Context
使用Context方式进行数据的共享,各个树状组件均可通过统一的Consumer统一访问全局的Consumer共享数据。
怎么样使用Context?
Context的使用主要分为创建、插入、访问三个流程,通过创建context对象提供共享组件,将Provider指定嵌套至需要使用共享数据的顶层结构,然后各后代组件通过context访问共享数据(变量、常量、方法等)。接下来,通过一个封装的登录组件来学习使用Context。
创建一个context
首先,我们需要创建一个待使用的context对象放在一边,等待使用的使用导入,我们可以将其封装成一个单独的js文件并将其export:
// 创建文件LoginContext.js
import { createContext } from 'react';
const LoginContext = createContext({});
export default LoginContext;
Provider包装一个组件
为了降低程序各个功能块的耦合性,防止Context共享数据的滥用和误用。一般的,我们可以将单独的一个模块(需要使用共享数据),进行一层封装,封装成一个单独的组件使用,如全局的多语言切换就可以在顶级组件App内封装。
//创建一个LoginFrom.js
import { Tabs, Form } from 'antd';
import React, { useState } from 'react';
import useMergeValue from 'use-merge-value';
import classNames from 'classnames';
import LoginContext from './LoginContext';
import LoginItem from './LoginItem';
import LoginSubmit from './LoginSubmit';
import LoginTab from './LoginTab';
import styles from './index.less';
const Login = props => {
const { className } = props;
const [tabs, setTabs] = useState([]);
const [active, setActive] = useState();
const [type, setType] = useMergeValue('', {
value: props.activeKey,
onChange: props.onTabChange,
});
const TabChildren = [];
const otherChildren = [];
React.Children.forEach(props.children, child => {
if (!child) {
return;
}
if (child.type.typeName === 'LoginTab') {
TabChildren.push(child);
} else {
otherChildren.push(child);
}
});
return (
<LoginContext.Provider
value={{
tabUtil: {
addTab: id => {
setTabs([...tabs, id]);
},
removeTab: id => {
setTabs(tabs.filter(currentId => currentId !== id));
},
},
updateActive: activeItem => {
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
setActive(active);
},
}}
>
<div className={classNames(className, styles.login)}>
<Form
form={props.from}
onFinish={values => {
if (props.onSubmit) {
props.onSubmit(values);
}
}}
>
{tabs.length ? (
<React.Fragment>
<Tabs
animated={false}
className={styles.tabs}
activeKey={type}
onChange={activeKey => {
setType(activeKey);
}}
>
{TabChildren}
</Tabs>
{otherChildren}
</React.Fragment>
) : (
props.children
)}
</Form>
</div>
</LoginContext.Provider>
);
};
Login.Tab = LoginTab;
Login.Submit = LoginSubmit;
Login.UserName = LoginItem.UserName;
Login.Password = LoginItem.Password;
Login.Mobile = LoginItem.Mobile;
Login.Captcha = LoginItem.Captcha;
export default Login;
从上面的代码我们可以看到LoginContext.Provider 提供了一个共享对象:
<LoginContext.Provider
value={{name:'title',value:'Provider'}}
>
......
</LoginContext.Provider>
一般的我们通过设置value值来指定共享数据的内容。
调用共享数据
通过上面的创建和定义,接下来我们就可以在Provider包含组件内进行共享数据的调用获取,而不需要一层层的去传递了。
//创建文件LoginTab.js
import React, { useEffect } from 'react';
import { Tabs } from 'antd';
import LoginContext from './LoginContext'; // 创建的共享context
const { TabPane } = Tabs;
const generateId = (() => {
let i = 0;
return (prefix = '') => {
i += 1;
return `${prefix}${i}`;
};
})();
const LoginTab = props => {
useEffect(() => {
const uniqueId = generateId('login-tab-');
const { tabUtil } = props;
if (tabUtil) {
tabUtil.addTab(uniqueId);
}
}, []);
const { children } = props;
return <TabPane {...props}>{props.active && children}</TabPane>;
};
const WrapContext = props => (
<LoginContext.Consumer>
{value => <LoginTab tabUtil={value.tabUtil} {...props} />}
</LoginContext.Consumer>
); // 标志位 用来判断是不是自定义组件
WrapContext.typeName = 'LoginTab';
export default WrapContext;
上面的代码可以看出,我们可以通过LoginContext.Consumer包裹器包裹获取共享数据Context,其获取数据格式:
<LoginContext.Consumer>
{value => (/**这里可以渲染对应的jsx代码,value就是Provider中提供的value**/)}
</LoginContext.Consumer>