特殊的ref

在 2.6.2 节非受控组件中,已经使用过 ref 来获取表单元素。ref 不仅可以用来获取表单元素,还可以用来获取其他任意 DOM 元素,甚至可以用来获取 React 组件实例。在一些场景下,ref 的使用可以带来便利,例如控制元素的焦点、文本的选择或者和第三方操作 DOM 的库集成。但绝大多数场景下,应该避免使用 ref,因为它破坏了 React 中以 props 为数据传递介质的典型数据流。本节将介绍 ref 常用的使用场景。

在DOM元素上使用ref

在 DOM 元素上使用 ref 是最常见的使用场景。ref 接收一个回调函数作为值,在组件被挂载或卸载时,回调函数会被调用,在组件被挂载时,回调函数会接收当前 DOM 元素作为参数;在组件被卸载时,回调函数会接收 null 作为参数。例如:

class AutoFocusTextInput extends React.Component {
    componentDidMount() {
        // 通过 ref 让 input 自动获取焦点
        this.textInput.focus();
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    ref={(input) => { this.textInput = input; }}
                />
            </div>
        )
    }
}

AutoFocusTextInput 中为 input 元素定义 ref,在组件挂载后,通过 ref 获取该 input 元素,让 input 自动获取焦点。如果不使用 ref,就难以实现这个功能。

在组件上使用ref

React 组件也可以定义 ref,此时 ref 的回调函数接收的参数是当前组件的实例,这提供了一种在组件外部操作组件的方式。例如,在使用 AutoFocusTextInput 组件的外部组件 Container 中控制 AutoFocusTextInput:

class AutoFocusTextInput extends React.Component {
    constructor(props) {
        super(props);
        this.blur = this.blur.bind(this);
    }

    componentDidMount() {
        // 通过 ref 让 input 自动获取焦点
        this.textInput.focus();
    }

    // 让 input 失去焦点
    blur() {
        this.textInput.blur();
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    ref={(input) => {this.textInput = input;}}
                />
            </div>
        )
    }
}

class Container extends React.Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        // 通过 ref 调用 AutoFocusTextInput 组件的方法
        this.inputInstance.blur();
    }

    render() {
        return (
            <div>
                <AutoFocusTextInput ref={(input) => {this.inputInstance = input}} />
                <button onClick={this.handleClick}>失去焦点</button>
            </div>
        );

    }
}

在 Container 组件中,我们通过 ref 获取到了 AutoFocusTextInput 组件的实例对象,并把它赋值给 Container 的 inputInstance 属性,这样就可以通过 inputInstance 调用 AutoFocusTextInput 中的 blur 方法,让已经处于获取焦点状态的 input 元素失去焦点。

注意,只能为类组件定义 ref 属性,而不能为函数组件定义 ref 属性,例如下面的写法是不起作用的:

function MyFunctionalComponent() {
  return <input />;
}

class Parent extends React.Component {
    render() {
        // ref 不生效
        return (
            <MyFunctionalComponent ref={(input) => {this.textInput = input; }} />
        );
    }
}

函数组件虽然不能定义 ref 属性,但这并不影响在函数组件内部使用 ref 来引用其他 DOM 元素或组件,例如下面的例子是可以正常工作的:

function MyFunctionalComponent() {
  let textInput = null;

  function handleClick() {
      textInput.focus();
  }

  return (
      <div>
        <input
            type="text"
            ref={(input) => {textInput = input; }}
        />
        <button onClick={handleClick} >获取焦点</button>
      </div>
  )
}

父组件访问子组件的DOM节点

在一些场景下,我们可能需要在父组件中获取子组件的某个 DOM 元素,例如父组件需要知道这个 DOM 元素的尺寸或位置信息,这时候直接使用 ref 是无法实现的,因为 ref 只能获取子组件的实例对象,而不能获取子组件中的某个 DOM 元素。不过,我们可以采用一种间接的方式获取子组件的 DOM 元素:在子组件的 DOM 元素上定义 ref,ref 的值是父组件传递给子组件的一个回调函数,回调函数可以通过一个自定义的属性传递,例如 inputRef,这样父组件的回调函数中就能获取到这个 DOM 元素。下面的例子中,父组件 Parent 的 inputElement 指向的就是子组件的 input 元素。

function Children(props) {
    // 子组件使用父组件传递的 inputRef, 为 input 的 ref 赋值
    return (
        <div>
            <input ref={props.inputRef} />
        </div>
    );
}

class Parent extends React.Component {
    render() {
        // 自定义一个属性 inputRef,值是一个函数
        return (
            <Children inputRef={el => this.inputElement = el} />
        );
    }
}

从这个例子中还可以发现,即使子组件是函数组件,这种方式同样有效。