React父组件主动调用子组件函数

Posted by wangtiegang on March 7, 2020

前几天在写一个比较复杂的表单页面时,碰到了一些难题,页面总体是一个大表单,这个大表单根据条件组合其他几个小表单,提交时,分别收集小表单的值,然后校验通过之后提交。这就存在一个问题,在大表单上点击提交按钮时,如何获得各个子表单的值。在我有限的 React 认知下,我只知道组件之间通讯,通过父组件传递函数到子组件,然后子组件调用返回信息,父组件没办法主动调用。但是显然是有办法实现父组件主动调用的方式,没办法,又到了通过问题学习知识的时候了!首先想到的就是 HOOK 中的 useRef ,看起来就是引用啊,于是去看 React 的官方文档,然后就发现了 useRefuseImperativeHandle,之前过了一眼,来不及仔细看,是时候仔细看看了。

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。组件的 ref 属性会返回该组件的 DOM 对象,因此可以通过 ref 去操作 DOM。useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值。

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。也就是说可以通过父组件传递一个 ref 给子组件,子组件把暴露的对象给 ref,父组件就可以直接通过 ref 调用被暴露的函数了,但是官方文档指出 useImperativeHandle 应当与 forwardRef 一起使用,这个给我后面增加了很多困扰。

定义一个子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ref 为父组件的传递过来
function Child(props, ref) {
  
  // 函数返回值为暴露的对象
  useImperativeHandle(ref, () => ({
    log: () => {
      console.log('test');
    }
  }));

  return <Input />;
}
// forwardRef使用之后,父组件的ref会被传递到函数式组件的第二个参数
export default React.forwardRef(Child);

在父组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
// ref 为父组件的传递过来
function Parent() {
  // 获得一个 ref
  const ref = useRef();
  
  const handleClick = () => {
      //通过 ref 主动调用子组件的函数
      ref.current.log();
  }
  // 将ref传递给子组件
  return <Child ref={ref}/>;
}

按照上面的方式,就可以很方便的调用子组件暴露的函数了,但是我在使用时碰到了一个问题。我的项目是基于 antd pro 的,它使用 dva 封装将组件封装成高阶组件,从而方便的将数据提升到上层 modal 中处理。在上面的例子中,要转发父组件的 ref 到子组件,就必须通过 React.forwardRef包装组件,那就必须跟 connect 同时使用了,实际使用之后发现会报错,connect 返回的高阶组件不能作为 React.forwardRef 的参数。这就头疼了,虽然 React 官网也写了如何在高阶组件中使用 useImperativeHandle,但是不知道怎么跟 dva 结合起来。

查阅了很多资料去解决,但是方法很复杂,感觉耗不起这个时间了,最后我采用了一种不遵循 useImperativeHandle 应当与 forwardRef 一起使用 的方式。React.forwardRef 是将父组件的 ref 属性直接通过函数的第二个参数传递下去,如果是这样的话,可以直接通过 props 属性去传递,这样就不用 forwardRef 了,只是不能使用 ref 这个属性名了而已。方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ref 通过 prps 传递过来
function Child(props) {
  const { childRef } = props;
  // 函数返回值为暴露的对象
  useImperativeHandle(childRef, () => ({
    log: () => {
      console.log('test');
    }
  }));

  return <Input />;
}
// 不使用 forwardRef,避免跟 connect 同时使用报错
export default @connect(({ loading }) => ({
  dataSourceLoading: loading.effects['xxx/xxx'],
}))(Child);

// 父组件调用
function Parent() {
  // 获得一个 ref
  const ref = useRef();
  
  const handleClick = () => {
      //通过 ref 主动调用子组件的函数
      ref.current.log();
  }
  // 属性名不能使用 ref,使用其他名字
  return <Child childRef={ref}/>;
}