React 之 forwardRef & useImperativeHandle

Posted by violetks on August 15, 2020

一、forwardRef

React.forwardRef会创建一个 React 组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。
React.forwardRef接受渲染函数作为参数。React 将使用propsref作为参数来调用此函数。此函数应返回 React 节点。

// 通过 forwardRef 创建子组件
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>

));

// 调用子组件
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

在上述的示例中,React 会将<FancyButton ref={ref}>元素的ref作为第二个参数传递给React.forwardRef函数中的渲染函数。该渲染函数会将ref传递给<button ref={ref}>元素。
因此,当 React 附加了ref属性之后,ref.current将直接指向<button> DOM 元素实例。

二、useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值。useImperativeHandle应当与forwardRef一起使用:

function FancyInput (props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染<FancyInput ref={inputRef} />的父组件可以调用inputRef.current.focus()

三、使用场景

使用步骤条处理事件时,每一步有一个表单,可分别定义为一个组件,并且有一个共同的父组件。
可以通过forwardRef创建子组件,在子组件中使用useImperativeHandle方法将每一步的表单值暴露给父组件。

// 父组件
import React, { useEffect, useState, useRef } from 'react';
import { useDispatch } from 'dva';
import { Button, Steps, Row, Space } from 'antd';

const { Step } = Steps;

const Parent = () => {
  const dispatch = useDispatch();

  const [step, setStep] = useState(0);
  const [valueOne, setValueOne] = useState({});      // 子组件第一步表单值
  const [valueTwo, setValueTwo] = useState({});      // 子组件第二步表单值
  const [valueThree, setValueThree] = useState({});  // 子组件第三步表单值

  // 子组件ref
  const stepOne = useRef();
  const stepTwo = useRef();
  const stepThree = useRef();

  const nextStep = () => {
    switch (step) {
      case 0: stepOne.current.nextStep(); break;
      case 1: stepTwo.current.nextStep(); break;
      case 2: stepThree.current.commit(); break;
      default: break;
    }
  }

  return (
    <>
      <Steps current={step} className={styles.steps_btn}>
        <Step key="0" title="第一步页面" />
        <Step key="1" title="第二步页面" />
        <Step key="2" title="第三步页面" />
      </Steps>
      {step === 0 &&
        <StepOne ref={stepOne} setStep={setStep} valueOne={valueOne} setValueOne={setValueOne} taskId={taskId} />}
      {step === 1 &&
        <StepTwo ref={stepTwo} setStep={setStep} valueOne={valueOne}
          valueTwo={valueTwo} setValueTwo={setValueTwo} taskId={taskId} />}
      {step === 2 &&
        <StepThree ref={stepThree} setStep={setStep} valueOne={valueOne} valueTwo={valueTwo}
          valueThree={valueThree} setValueThree={setValueThree} taskId={taskId} />}
    </>
    <div className={styles.steps_action}>
      <Row justify="center">
        <Space size={12}>
          {step > 0 && <Button onClick={() => setStep(i => i - 1)}>上一步</Button>}
          {step < 2 && <Button type="primary" onClick={nextStep}>下一步</Button>}
          {step === 2 && <Button type="primary" onClick={nextStep} >提交</Button>}
        </Space>
      </Row>
    </div>
  )
}

export default Parent;
// 子组件
import React, { useImperativeHandle, forwardRef } from 'react';
import { Form } from 'antd';

const StepOne = ({ setStep, setValueOne, valueOne, taskId }, ref) => {
  const [form] = Form.useForm();

  // 将子组件的方法向上暴露,将下一步方法暴露给父组件
  useImperativeHandle(ref, () => ({
    nextStep: () => {
      form.validateFields().then(values => {
        setValueOne(params);  // 表单值传给父组件
        setStep(i => i + 1);
      })
    }
  }));

  return (
    <Form form={form} {...formItemLayout}>
      ...
    </Form>
  )
}

export default forwardRef(StepOne);