React - Provider使用教程(context)、 react自带代替redux的方案

11,000 阅读5分钟

上面说代替redux指的是,不是所有跨组件的数据都用redux。比如主题就不适合用redux了

我自认为Provider的优点

    1. 解决了组件数据多层传递问题、多层组件交互问题
    1. 多个根数据,这个直接和redux区分开了, 很适合做主题. provider也是React自带,不需要安装
    1. 范围更明确,如果redux数据丢太多了,太重了
    1. 更轻量,没有redux多么多复杂的操作

文章先送下官方文档哈

React Context官方中文文档

正文开始,如不喜欢看文档的方式,可直接滑动到下面【基本使用例子】

基本方法

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>
  );
};

效果图

61440d43196d4_61440d4333831.gif

-- 完 --