React Hooks 的 useContext + useReducer + useImperativeHandle + forwardRef实战场景

347 阅读1分钟

有时候层层传递的确有失优雅

如果你的项目父子组件传值太繁琐,用redux又觉得太重,考虑用context + useReducer吧

以下代码将会出现父、子、孙三级组件,运用 useContext + useReducer 的组合实现多层级的数据通信

基本的嵌套关系是这样的

<App>
    <Description>
        <DescriptionEdit />
    </Description>
</App>

App-父组件

import React, { useReducer } from 'react';
import { useEffect } from 'react';
import { Description, ChangeRequest, VisitedLink, Team } from '@/components/AppDetails';
import { Col, Row, Space } from 'antd';
import { getAppItemDetails, getOcrListOfAppItemDetails } from '@/service/app';
import { useParams } from '@route/runtime';

export const UPDATE_DESCRIPTION = 'UPDATE_DESCRIPTION';
export const UPDATE_CHANGE_REQUEST = 'UPDATE_CHANGE_REQUEST';

function reducer(state, action) {
  switch(action.type) {
    case UPDATE_DESCRIPTION:
      return { ...state, description: action.description };
    case UPDATE_CHANGE_REQUEST:
      return { ...state, changeRequest: action.changeRequest };
    default:
      return state;
  }
}
export const AppContext = React.createContext({});

const App = () => {
  const [data, dispatch] = useReducer(reducer, { description: {}, changeRequest: {} });
  const { id } = useParams() as { id: string };

  async function initApi() {
    const res = await Promise.all([
      getAppItemDetails(id),
      getOcrListOfAppItemDetails({
        softwareProject: id,
        open: true,
        size: 10
      })
    ])
    console.log('--- * res * --- : ', res);
    if (res) {
      dispatch({ type: UPDATE_DESCRIPTION, description: res[0] });
      dispatch({ type: UPDATE_CHANGE_REQUEST, changeRequest: res[1] });
    }
  }

  useEffect(() => {
    initApi();
  }, []);

  return (
    <AppContext.Provider value={{ data, dispatch }}>
      <Space direction="vertical" size="middle" style={{ display: 'flex' }}>
        <Description title="urbanic-app-a" />
        <Row gutter={16}>
          <Col span={16}>
            <ChangeRequest />
          </Col>
          <Col span={8}>
            <Space direction="vertical" size="middle" style={{ display: 'flex' }}>
              <VisitedLink />
              <Team />
            </Space>
          </Col>
        </Row>
      </Space>
    </AppContext.Provider>
  )
};

export default App;


App-子组件 在子组件中会控制内部Modal弹框的显隐,使用 useImperativeHandle + forwardRef 实现

import { Breadcrumb, Card, message } from 'antd';
import { StarFilled } from '@ant-design/icons';
import { useContext, useRef } from 'react';
import DescriptionEdit from './DescriptionEdit';
import copy from 'copy-to-clipboard';
import { GitLabIcon,RepoIcon,TechIcon } from '@/icons';
import { AppContext } from '@/pages/app/$id';

interface Props {
  title: string;
}
const Description = (props: Props) => {
  const { title } = props;
  const editRef = useRef(null) as any;
  const { data } = useContext(AppContext) as ContextType;
  const { description } = data;

  return (
    <Card>
      <div className="flex">
        {/* 面包屑和编辑事件 */}
        <Breadcrumb style={{ marginBottom: 10 }}>
          <Breadcrumb.Item>Home</Breadcrumb.Item>
          <Breadcrumb.Item>
            <a href="">Apps</a>
          </Breadcrumb.Item>
          <Breadcrumb.Item>
            <a href="">{ title }</a>
          </Breadcrumb.Item>
        </Breadcrumb>
        <a onClick={() => editRef?.current?.setIsModalOpen(true)}>Edit</a>
      </div>
      <div>
        {/* 左侧描述部分 */}
        <div>
          <div className="flexStart mb20">
            <span style={{ marginRight: 4 }} className="title">{ title }</span>
            {/* 星标的颜色取决于是否收藏 */}
            <StarFilled style={{ color: '#1890ff', fontSize: 12, paddingTop: 3 }} />
          </div>
          <div className="mb20" style={{ width: '70%' }}>{description?.description}</div>
          <div className="flexStart">
            <div className="mr20 flexStart">
              <div className="mr5"><RepoIcon/></div>
              <a onClick={() => {
                copy(description?.codeRepo);
                message.success('复制成功');
              }}>Git-repo</a>
            </div>
            <div className="mr20 flexStart">
              <div className="mr5">
                <GitLabIcon />
              </div>
              <a onClick={() => window.open(description?.codeHomePage)}>Gitlab</a>
            </div>
            <div className="mr20 flexStart">
              <div className="mr5">{ <TechIcon /> }</div>
              <a onClick={() => alert('跳转到应用列表页面')}>技术质量部门</a>
            </div>
          </div>
        </div>
        {/* 右侧图片 */}
         <img src="" alt=""/>
      </div>
      <DescriptionEdit
        ref={editRef}
        title={title}
      />
    </Card>
  )
}

export default Description;

App-孙组件 孙组件内包含Modal框、编辑交互、reload数据的操作等


import { forwardRef, useImperativeHandle, useState, useContext } from 'react';
import { Modal, Form, Input, Button, message } from 'antd';
import { useParams } from '@ice/runtime';
import { AppContext, UPDATE_DESCRIPTION } from '@/pages/app/$id';
import { getAppItemDetails, updateAppItemDetails } from '@/service/app';

interface Props {
  title: string;
}

const tailLayout = {
  wrapperCol: { offset: 19 },
};

const DescriptionEdit = forwardRef((props: Props, ref) => {
  const { title } = props;
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [form] = Form.useForm();
  const { id } = useParams() as { id: string };
  const { data, dispatch } = useContext(AppContext) as ContextType;
  const { description } = data;

  useImperativeHandle(ref, () => {
    return {
      setIsModalOpen
    }
  });

  const handleOk = () => {
    setIsModalOpen(false);
  };

  const handleCancel = () => {
    setIsModalOpen(false);
  };

  const onFinish = async (values: { codeRepo: string, description:string }) => {
    const newData = {...description, codeRepo: values?.codeRepo,  description: values?.description };
    // --
    const res = await updateAppItemDetails(id, newData);
    if (res?.status == 200) {
      const res = await getAppItemDetails(id);
      dispatch({ type: UPDATE_DESCRIPTION, description: res });
    } else {
      message.error(res?.statusText);
    }
    // --
    setIsModalOpen(false);
  };

  const onReset = () => {
    form.resetFields();
    setIsModalOpen(false);
  };


  return (
    <Modal
      width="50%"
      title={title}
      open={isModalOpen}
      onOk={handleOk}
      onCancel={handleCancel}
      footer={false}
    >
      {/* Gitlab地址 和 简介 是可编辑的 */}
      <Form
        form={form}
        onFinish={onFinish}
        labelCol={{ span: 4 }}
        wrapperCol={{ span: 20 }}
        name="DescriptionEdit"
        initialValues={{ 
          codeRepo: description?.codeRepo ?? 'initGitlab',
          description: description?.description ?? 'initDescription'
        }}
        autoComplete="off"
      >
        <Form.Item
          label="Description"
          name="description"
          rules={[{ required: true, message: 'Please input your description!' }]}
        >
          <Input.TextArea />
        </Form.Item>

        <Form.Item
          label="Gitlab"
          name="codeRepo"
          rules={[{ required: true, message: 'Please input your gitlab!' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item {...tailLayout}>
          <Button style={{ marginRight: 10 }} type="primary" htmlType="submit">
            确认
          </Button>
          <Button htmlType="button" onClick={onReset}>
            取消
          </Button>
        </Form.Item>
      </Form>
    </Modal>
  )
})

export default DescriptionEdit;