React函数式组件使用同时转发子组件的ref

use forward ref

React 中常常会用到 Ref 对组件进行命令式的调用,官方对不同 ref 值的介绍如下:

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。

但是函数式组件,可以使用 forwardRef 与 useImperativeHandle 组合使用,来实现对外暴露组件调用命令的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// cool-input.tsx
const CoolInput = forwardRef<CoolInputForward, CoolInputProps>((props, forwardedRef) => {
const [value, setValue] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null);

// 对外暴露命令式方法
useImperativeHandle(forwardedRef, () => ({
getValue() {
return value;
},
// 聚焦Input框
focusInput() {
inputRef.current?.focus();
},
}));

return (
<input ref={inputRef} value={value} onChange={({ target: { value } }) => setValue(value)} />
);
});

同时,使用 forwardRef,可以实现将函数式组件中的子组件转发给父组件使用,让父组件直接调用对应的命令。

1
2
3
4
5
6
7
// cool-input.tsx
const CoolInput = forwardRef<HTMLInputElement, CoolInputProps>((props, forwardedRef) => {
const [value, setValue] = useState<string>("");
return (
<input ref={forwardedRef} value={value} onChange={({ target: { value } }) => setValue(value)} />
);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.tsx
const Index: React.FC<IndexProps> = () => {
const coolInputRef = useRef<HTMLInputElement>(null);
const onFocusCool = useCallback(() => {
coolInputRef.current?.focus();
}, []);
return (
<div>
<CoolInput ref={coolInputRef} />
<button onClick={onFocusCool}>聚焦输入框</button>
</div>
);
};

这样就可以在父组件中,直接调用函数组件中的子组件的事件了。但是有一种特殊的情况:函数组件中在转发子组件的同时,也需要使用该组件的 Ref。我们期望实现的效果是,父组件(index.tsx)点击「聚焦输入框」可以正确的使 input 触发 focus 事件,同时,函数组件也可以监听 input 的 focus 事件,打印输入框的值。

这个时候,我们便需要将 input 元素同时赋给多个 Ref,因此需要根据 ref 的类型来进行批量赋值。ref 赋值有三种方式:

1. 字符串定义

不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除

1
2
3
4
<input type='text' ref='textInput' />;

// 使用ref
this.refs.textInput;

2. 使用回调函数

1
2
3
4
5
this.setTextInputRef = (element) => {
this.textInput = element;
};

<input type='text' ref={this.setTextInputRef} />;

3. 使用 Ref 对象

在 React 16.3 版本之后,使用 createRef 使得对象引用变得更加方便。因此正常情况下,推荐使用该方式。

1
2
3
4
5
// 声明
this.textInput = React.createRef();

// 赋值
<input type='text' ref={this.textInput} />;

在函数组件中,一样可以在函数组件内部使用 ref 属性,使用方法便是 useRef,创建一个 Ref 对象。

1
2
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} type='text' />;

回归问题,我们只需要判断 ref 的类型,然后根据当前类型,依次赋值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const CoolInput = forwardRef<HTMLInputElement, CoolInputProps>((props, forwardedRef) => {
const inputRef = useRef<HTMLInputElement>(null); // Object类型的Ref

/** 根据ref类型设置ref的值 */
function setRef(ref: React.Ref<HTMLInputElement>, value: HTMLInputElement | null) {
if (typeof ref === "function") {
ref(value);
} else if (ref !== null) {
(ref as MutableRefObject<HTMLInputElement | null>).current = value;
}
}

return (
<input
ref={(instance) => {
// 依次设置ref的值
setRef(forwardedRef, instance);
setRef(inputRef, instance);
}}
onFocus={() => console.log(inputRef.current?.value)}
/>
);
});

思路便是以上的内容,即将示例依次赋值给对应的 ref 即可,但是这样编写起来比较麻烦,我可以将该功能提取为一个 hooks 使用,这样就不需要单独去赋值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { MutableRefObject, Ref, useCallback } from "react";

/**
* 合并多个ref为一个
*/
export const useCombinedRefs = <T extends any>(...refs: Array<Ref<T>>): Ref<T> =>
useCallback(
(element: T) =>
refs.forEach((ref) => {
if (!ref) {
return;
}
if (typeof ref === "function") {
return ref(element);
}
(ref as MutableRefObject<T>).current = element;
}),
refs
);

这样我们即可在页面中直接使用即可合并所有相同的 Ref 为一个,直接赋值到元素上即可。

1
2
3
4
5
6
const CoolInput = forwardRef<HTMLInputElement, CoolInputProps>((props, forwardedRef) => {
const inputRef = useRef<HTMLInputElement>(null); // Object类型的Ref
const combineRef = useCombinedRefs(forwardedRef, inputRef);

return <input ref={combineRef} onFocus={() => console.log(inputRef.current?.value)} />;
});

这时,父组件也可以直接对 input 元素使用.focus()方法,函数组件中也可以随时在 onFocus 中使用 ref 打印当前输入框的值了。

参考