上面说代替redux指的是,不是所有跨组件的数据都用redux。比如主题就不适合用redux了
我自认为Provider的优点
-
- 解决了组件数据多层传递问题、多层组件交互问题
-
- 多个根数据,这个直接和redux区分开了, 很适合做主题. provider也是React自带,不需要安装
-
- 范围更明确,如果redux数据丢太多了,太重了
-
- 更轻量,没有redux多么多复杂的操作
文章先送下官方文档哈
正文开始,如不喜欢看文档的方式,可直接滑动到下面【基本使用例子】
基本方法
React.createContext
创建一个上下文, 当组件渲染匹配从Provider
匹配到了最近的上下文,就会使用它的值。
const MyContext = React.createContext(defaultValue);
MyContext里面有Provider、Consumer属性,Provider就是在外层使用的组件(遵守上下文规则)
比如,一个页面黑色主题,有一部分是白色主题,这时候结构如下
<XXProvider value={黑色}>
。。。
<XXProvider value={白色}>
...
</XXProvider>
。。。
</XXProvider>
Context.Provider
这个就是初始化和在业务使用的组件,接受一个value的值,当value的值发生改变的时候,会重新渲染下面的数据。
Class.contextType
这个不常使用,这个是破坏规则的,正常来说组件遵守的是Provider的数据,但是如果手动修改制定也是可以的。 如下官方示例。
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;
Context.Consumer
上面指的是如何创建一个context,这个就是如何使用它。这个是一个组件函数,会给下面的函数传递参数,源码应该如下
const Consumer = ({children}) => {
const values = '我有一个值'
return children(values)
}
使用方式
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
Context.displayName
主要是为了配合浏览器插件(React DevTools),给开发使用的
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
基本使用例子
我们实现一个数据逆传递的例子
设想一下我们有一个钱包页面,页面下面有一些卡片,卡片里面有一个弹窗。
这时候页面的组件关系为
Wallet -> Card -> Dialog
目的
我们在Wallet里面获取数据,然后把数据通过context传递给Dialog
实现
先在合适的位置创建一个provider文件,然后创建一个context并导出
interface IAccountContext {
imageList: IImageListType[];
}
const accountContextDefaultValue = {
imageList: [],
}
export const AccountContext = createContext<IAccountContext>(
accountContextDefaultValue,
);
const { Consumer: AccountConsumer, Provider: AccountProvider } = AccountContext;
export { AccountProvider, AccountConsumer };
编写完成后就可以找地方随便使用啦,这时候我们在Wallet页面使用它
import {AccountProvider} from './accountProvider.tsx'
const WalltePage = () => {
return <AccountProvider
value={{
imageList: [1],
}}
>
<Card />
</AccountProvider>
}
这时候provider完成后,下面的任何地方都可以很方便使用。使用方式有两种,一种是Consumer
, 一种是Hooks 函数.
const Dialog = () => {
return <AccountConsumer>
{({imageList}) => {
return <div>dialog</div>
}}
</AccountConsumer>
}
这样就可以在组件里面使用啦,但是不太方便js使用,如果是点击的话,可以传参数,如果是js一开始就需要用,这个方式就不太好用了。
hooks
Dialog.tsx
const Dialog = () => {
const { imageList } = useContext(AccountContext);
return <div>Dialog</div>
}
到这里 走完流程了 但是如果我们修改动态数据修改怎么办?比如说我们想要卡片内部的弹窗有一个按钮,点击后可以卡片本身。
高级例子
实现这个例子,我们需要在context值加一个钩子函数,提供给某一个组件内部调用, 这里基于上面的代码添加吧。
这里我们基于上面代码添加一个功能,就是在弹窗内部加一个按钮,按钮上面可以点击开启,开启后会有抽卡片的动效,但是动效本身是超越弹窗而存在的。也就是数据是寄存在context上面。
先修改context
context文件
import { IImageListType } from '@/service/type';
import { createContext } from 'react';
export type AccountContextUpdateOpenBoxTempInfo = (option: {
id: string;
img: string;
open: boolean;
}) => void;
interface openingBoxInfo {
openBoxTempImage: string;
isOpeningBox: boolean;
openingBoxId: string;
updateOpenBoxTempInfo: AccountContextUpdateOpenBoxTempInfo;
}
interface IAccountContext extends openingBoxInfo {
imageList: IImageListType[];
boxOpenAt: number;
reloadFetch: () => void;
}
const accountContextDefaultValue = {
imageList: [],
boxOpenAt: 0,
reloadFetch: () => {},
openBoxTempImage: '',
openingBoxId: '',
isOpeningBox: false,
updateOpenBoxTempInfo: () => {},
};
export const AccountContext = createContext<IAccountContext>(
accountContextDefaultValue,
);
const { Consumer: AccountConsumer, Provider: AccountProvider } = AccountContext;
export { AccountProvider, AccountConsumer };
写好connext后只需要在provider提供具体逻辑即可
const Wallet: React.FC<IWalletProps> = ({}) => {
const [boxOpenAt, setBoxOpenAt] = useState(0);
const reloadFetch = async () => {
setCurrentPage((currentPage = defaultPageNum));
fetchList();
};
const fetchList = async () => {
const overData = await fetchOverviewList({ ts, account: account ?? '', status, pageNum: currentPage, pageSize: 9, });
setOverViewMockList(
currentPage === defaultPageNum ? overData.list : [...overViewMockList, ...overData.list],
);
setTotalPage(overData.total);
};
const [openBoxTempImage, useOpenBoxTempImage] = useState('');
const [openingBoxId, setOpeningBoxId] = useState('');
const [isOpeningBox, useIsOpeningBox] = useState(false);
// 注意这行代码,钩子函数提供给context,最后组件调用钩子函数
const updateOpenBoxTempInfo: AccountContextUpdateOpenBoxTempInfo = ({
img,
open,
id,
}) => {
// context数据可能实时修改,是依靠hook数据,如果是class组件,就setData,官方就是class的例子
useOpenBoxTempImage(img);
useIsOpeningBox(open);
setOpeningBoxId(id);
};
return <AccountProvider
value={{
imageList,
boxOpenAt,
reloadFetch,
openingBoxId,
openBoxTempImage,
isOpeningBox,
updateOpenBoxTempInfo,
}}
>
{/* CODE */}
<Card />
</AccountProvider>
}
Dialog 文件, 在这里,我们把Dialog抽出来,因为Dialog本身是纯粹的弹窗,弹窗内部会有不同状态生成不同的内容,所以内容,我们就叫他UnOpanBox吧。,也就是在这里去触发内容。
export const UnOpenBox: React.FC<{ onClose: () => void; id: number }> = ({
onClose,
id,
}) => {
const classes = useOpenBoxStyles();
const { reloadFetch, updateOpenBoxTempInfo } = useContext(AccountContext);
const onOpenBox = async () => {
updateOpenBoxTempInfo({ img: '', open: true, id: id.toString() });
await new Promise((ok) => setTimeout(ok, 5000));
reloadFetch();
}
return <div className={classes.root}>
<>
<Button
color="secondary"
fullWidth
variant="contained"
onClick={onOpenBox}
>
Yes
</Button>
<div style={{ height: 15 }}></div>
<Button
color="secondary"
fullWidth
variant="outlined"
onClick={onClose}
>
close
</Button>
</>
</div>
}
const useOpenBoxStyles = makeStyles({
root: {
padding: '15px',
},
});
到这里流程就走完了,数据已经修改了,对应的地方改变即可,这时候我们在Card里面去监头数据,再修改动效组件。
Card.tsx
export const ProductRevealCard: React.FC<IRevealCard> = ({
id,
url,
status,
title,
}) => {
const { isOpeningBox, openingBoxId } = useContext(AccountContext);
const [open, setOpen] = useState<boolean>(
status === CardStatusEnum.COUNTDOWN,
);
const onClose = () => {
setOpen(false);
};
let Child = <></>;
if (status === CardStatusEnum.UNOPENED) {
Child = <RenderUnOpenBox id={id} onClose={onClose} />;
}
if (status === CardStatusEnum.COUNTDOWN) {
Child = <RenderCountdownBox onClose={onClose} />;
}
const isOpenTemImg = Boolean(isOpeningBox && openingBoxId === id.toString());
return (
<div className={styles['card-box']}>
<ImageCard
onClick={() => {
setOpen(true);
}}
image={url}
title={title}
/>
<CardDialog
open={
isOpenTemImg
? false
: status === CardStatusEnum.OPENED
? false
: status === CardStatusEnum.PADDING
? true
: open
}
title={getCardDialogTitle(status)}
>
{Child}
</CardDialog>
{isOpenTemImg && <OpenBoxTemView />}
</div>
);
};
const getCardDialogTitle = (status: CardStatusEnum) => {
return (
<span>
{status === CardStatusEnum.COUNTDOWN
? 'Reveal Countdown'
: status === CardStatusEnum.PADDING
? 'NFT Is Minting'
: status === CardStatusEnum.UNOPENED
? 'Reveal Now'
: ''}
</span>
);
};
ok,整个流程完成,不过既然都细致到这个程度了,再把动效果代码贴上吧。
OpenBoxTemView.tsx
import { CardMedia } from '@material-ui/core';
import _ from 'lodash';
import { useContext, useEffect, useState } from 'react';
import { AccountContext } from '../../provide/accountProvide';
import { useOpenBoxTempStyle } from './openBoxTemStyles';
let run = false;
export const OpenBoxTemView: React.FC<{}> = () => {
const temClasses = useOpenBoxTempStyle();
const { imageList: list } = useContext(AccountContext);
const [openBoxTempImage, setOpenBoxTempImage] = useState('');
useEffect(() => {
run = true;
const loop = () => {
if (!run) return;
const item = list[_.random(0, list.length - 1)];
if (item) {
const url = item.image ?? item.url;
setOpenBoxTempImage(url);
setTimeout(() => {
loop();
}, 100);
}
};
loop();
return () => {
run = false;
};
}, []);
return (
<div className={temClasses.root}>
{openBoxTempImage && (
<CardMedia
className={temClasses.temImg}
component="img"
image={openBoxTempImage}
/>
)}
</div>
);
};
效果图
-- 完 --