如何在组件中使用React.forwardRef()和自己的ref?

9

任务
我正在尝试在组件中同时使用React.forwardRef()React.useRef()

问题
我只能使用其中之一,要么是myOwnRef,要么是来自forwardRef属性的ref。所以,我不知道如何将它们结合起来使用。 示例代码:

interface IExampleInputProps {
    value: string,
    onChange: (event: ChangeEvent<HTMLInputElement>) => void
}
const ExampleInput = forwardRef<HTMLInputElement | null, IExampleInputProps>(({value, onChange}, ref) => { // can't to use "ref" parameter
    const myOwnRef = React.useRef<HTMLInputElement | null>(null);

    return <input ref={el => {
        myOwnRef.current = el; // => OK
        if (ref) { // trying to use the ref passed from the forwardRef function
            ref.current = el; // => ERROR
        }
    }} value={value} onChange={onChange}/>
})

非常感谢您的回答。
2个回答

10
要在转发引用的同时访问它,可以按照以下简单的步骤进行操作:
  • 将在组件内部创建的引用附加到元素上。
  • 在外部引用上调用useImperativeHandle钩子,并传递一个函数,该函数返回内部引用的current属性的值,该值将被设置为外部引用的current属性的值。
import { forwardRef, useImperativeHandle, useRef } from 'react';
const MyComponent = forwardRef<HTMLInputElement, IExampleInputProps>(({value, onChange}, outerRef) => {
    const innerRef = useRef<HTMLInputElement>(null);
    useImperativeHandle(outerRef, () => innerRef.current!, []);
    // remember to list any dependencies of the function that returns the ref value, similar to useEffect
    return <input ref={innerRef} value={value} onChange={onChange} />;
});

为什么innerRef.current的末尾有一个感叹号(!)符号? - undefined
1
@mortadelo 这是给 TypeScript(非空断言)的。如果你不使用 TypeScript,应该将其删除。 - undefined

3

这个React文档的例子应该会有所帮助。

refmyOwnRef都应该是DOM元素(或能够接受ref的组件)的ref属性的值。

当像这样使用时,使用ExampleInput的父函数组件将通过调用React.useRef()来创建自己的Reactref

下面的示例代码的实时演示演示了一个示例。在此示例中,ParentComponent使用自己的refparentRef)直接作用于YourComponent中的

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>React.forwardRef test</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=1">
        <link rel="icon" href="data:;">
        <link rel="stylesheet" href="./css/index.css">
    </head>
    <body>
        <div id="testContainer" class="testContainer"></div>
        <script src="./index.js"></script>
        <noscript> Please enable javascript to view the site </noscript>
    </body>
</html>

index.jsx

/**
 * @file: index.jsx
 */
import React from 'react';
import ReactDOM from 'react-dom';
import './test.css';

const container = document.getElementById( 'testContainer' );

/**
 * This component is consumed by React.forwardRef().
 */
const YourComponent = ( { value, onChange }, ref ) => {
    const myOwnRef = React.useRef( null );
    const divRef = React.useRef( null );

    const doSomething = () => {
        myOwnRef.current.value = '';
    }

    const doSomethingElse = () => {
        if ( 'blink' == myOwnRef.current.value ) {
            divRef.current.classList.add( 'displayAnimatedBorder' );
        }
    }

    return (
        <div ref={ divRef }>
            <label
                ref={ ref }
                style={ { marginRight: '8px' } }
            >
                Field Label
            </label>
            <input
                ref={ myOwnRef }
                placeholder={ value }
                onChange={ onChange }
                onKeyUp={ doSomethingElse }
                onMouseDown={ doSomething }
            />
        </div>
    );
}


const ExampleInput = React.forwardRef( YourComponent );


/**
 * This parent component uses the ExampleInput component.
 */
function ParentComponent( props ) {
    const parentRef = React.useRef();

    const handleChange = () => {
        parentRef.current.classList.add( 'ColorChangeToIndicateFieldWasEdited' );
    }

    return (
        <div>
            <ExampleInput
                ref={ parentRef }
                value={ props.someValue }
                onChange={ handleChange }
            />
        </div>
    );
}

/**
 * Inject the parent component into the DOM.
 */
ReactDOM.render(
    <ParentComponent someValue={ 'Placeholder' } />,
    container
);

test.css

.ColorChangeToIndicateFieldWasEdited {
    background-color: yellow;
}

@keyframes blinky {
    0% {
        border: 8px solid rgba(255, 0, 0, 0);
    }
    100% {
        border: 8px solid rgba(255, 0, 0, 1);
    }
}

.displayAnimatedBorder {
    padding: 8px;
    width: fit-content;
    animation: 700ms ease 100ms 6 alternate none running blinky
}

更新

我已经更新了我的答案和实时演示,使用div的另一个示例。在input字段中输入文本“blink”会触发doSomethingElse回调函数,以闪烁边框更新div

针对这个问题:

如果我有一个“div”组件。我需要使用ref到这个组件。 更准确地说,是到同一个组件,而不是到这个div组件的包装器或任何子元素吗?

我认为使用单词ref存在混淆。在下面的文本中,我尝试使用React文档使用的格式约定:

  1. ref 通常指的是从调用函数 React.useRef() 返回的 React 对象,或者传递给由 React.forwardRef() 消费的组件。
  2. ref 指 JSX DOM 元素上的属性。
  3. ref 指我示例代码中传递给 YourComponent 的变量,由 React.forwardRef 处理。

你的问题似乎是在问:“如何在 YourComponent 中定义的 div 上使用 ref,而不将 ref 关联到 div 的任何父组件或子组件?” 如果是这样,那么这个问题显示了对 React.forwardRef 目的的误解。

React.forwardRef 用于使父组件直接访问位于由 React.forwardRef 消费的组件内部的特定 JSX DOM 元素(或组件)。使用 ref 来指示所需的 JSX DOM 元素(或组件)。

因此,在我的示例代码中,React.forwardRefParentComponent 直接提供了对位于 YourComponent 内部的 label 的访问。 YourComponentReact.forwardRef 使用。通过使用 React.forwardRefref parentRef 下传到 YourComponent 中。 YourComponent 的定义将变量名从 parentRef 更改为 ref,并将 ref 分配给 labelref 属性。此赋值声明我们要使用 parentRef 访问 label(而不是其他元素)。

React.forwardRef 不需要为直接访问 inputdiv 提供 refs。这是因为所有这些都在同一作用域中:

  1. 使用 React.useRef 创建的 refs myOwnRefdivRef
  2. 所需的 JSX DOM 元素;以及
  3. 回调函数 doSomethingdoSomethingElse,它们使用了这些 refs

ParentComponent 中使用 React.forwardRef 是有意义的,因为 handleChange 需要直接访问 label,而 handleChange 并不是在与 label 相同的作用域中定义的。


进一步解释...

参考

JSX DOM元素(例如我更新后的回答中的div)可以在其ref属性上使用ref(例如divRef)。我们之所以想要将ref传递给divref属性,是为了远程控制div。所谓远程控制,意思是不使用定义在div本身上的事件监听器,而是可以使用定义在另一个元素或组件上的事件监听器来控制div。例如,div的行为(闪烁边框)并未定义在div标签上。它是在从input标签上的onKeyUp事件监听器调用的doSomethingElse事件处理程序中定义的。

在这种情况下,我们不需要使用React.forwardRef,因为refdivRef)、divdoSomethingElse都在YourComponent的范围内声明。

React.forwardRef

但是,如果我们想要通过在父组件中定义的回调函数来影响DOM元素的行为(或访问其属性),该怎么办呢?这就是我们会使用React.forwardRef的时候。如果ParentComponent想要使用ref来控制YourComponent内部的label,它不能直接渲染YourComponent来实现。如果ParentComponent试图在YourComponent上使用ref属性,那个ref属性将无法让我们访问到所需的元素(label)。例如,以下方法将不起作用:
function ParentComponent( props ) {
    const parentRef = React.useRef();

    const handleChange = () => {
        parentRef.current.classList.add( 'ColorChangeToIndicateFieldWasEdited' );
    }

    return (
        <div>
            <YourComponent
                ref={ parentRef }
                value={ props.someValue }
                onChange={ handleChange }
            />
        </div>
    );
}

我们需要使用React.forwardRef来更新YourComponent(通过将其包装到ExampleInput中)以便YourComponent可以通过ExampleInput获取ref。然后将该ref传递给YourComponent,以确切地指定我们希望从ParentComponenthandleChange回调函数中控制哪个元素。
React文档页面使用Refs操纵DOM应该能提供帮助。

1
在这个例子中,你通过React.forwardRef()使用的ref被传递到label中,而myOwnRef被传递到了“input”组件中。因此,我们没有两个引用指向同一个组件。我仅仅为了举例子才创建了这个问题。如果我有一个“div”组件,我需要对它使用ref。更准确地说,是在同一个组件中,而不是在这个“div”组件的包装器或任何子元素中?或者我误解了什么? - Владимир Говоров

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接