React 父元件如何主動“聯絡”子元件

時傾 發表於 2021-11-26
React

在典型的 React 資料流中,props是父子元件互動的唯一方式。要修改一個子元件,需要使用新的 props 重新渲染它。但是,在某些情況下,需要在典型資料流之外主動檢視或強制修改子元件,這時候就需要使用 Refs,將 DOM Refs 暴露給父元件。

何時使用 Refs

下面是幾個適合使用 refs 的情況:

  • 管理焦點,文字選擇或媒體播放;
  • 觸發強制動畫;
  • 整合第三方 DOM 庫;
  • 測量子 DOM 節點的大小或位置;
避免使用 refs 來做任何可以通過宣告式實現來完成的事情,因為它會打破元件的封裝。

Refs 與元件

預設情況下,ref 屬性必須指向一個 DOM 元素或 class 元件,不能在函式元件上使用 ref 屬性,因為它們沒有例項。如果需要在函式元件中使用 ref,可以使用 forwardRef,或者將該元件轉化為 class 元件。

React.forwardRef

React.forwardRef(props, ref)

  • 第二個引數 ref 只在使用 React.forwardRef 定義元件時存在。常規函式和 class 元件不接收 ref 引數,且 props 中也不存在 ref
  • Ref 轉發不僅限於 DOM 元件,也可以轉發 refs 到 class 元件例項中。

Ref 轉發

如果使用 16.3 以上版本的 React,使用 ref 轉發將 DOM Refs 暴露給父元件。Ref 轉發使元件可以像暴露自己的 ref 一樣暴露子元件的 ref。如果對子元件的實現沒有控制權的話,只能使用 findDOMNode(),但在嚴格模式下已被廢棄且不推薦使用。

實現 Ref 轉發方式:

  • ref 和 forwardRef
  • useImperativeHandle 和 forwardRef

ref 和 forwardRef

  • 父元件建立 ref,並向下傳遞給子元件
  • 子元件通過 forwardRef 來獲取傳遞給它的 ref
  • 子元件拿到 ref並向下轉發該ref到自己的某一個元素上
  • 父元件通過 ref.current 獲取繫結的 DOM 元素例項

缺點

  • 會把繫結 ref 元素的 DOM 直接暴露給了父元件
  • 父元件拿到 DOM 後可以進行任意的操作

例子

下面的例子使用的是 React 16.6 引入的 useRef Hook,如果是 React 16.3 版本使用 React.createRef() API, 如果更早之前的版本,使用回撥形式的refs

// 子元件
import React, { forwardRef, useState } from 'react'

const BaseInfo = forwardRef((props, ref) => {
  console.log('BaseInfo --- props', props)
  console.log('BaseInfo --- ref', ref)

  const [name, setName] = useState('')
  const [age, setAge] = useState('')

  return (
    <div ref={ref}>
      姓名:
      <input onChange={val => setName(val)} />
      年齡:
      <input onChange={val => setAge(val)} />
    </div>
  )
})
// 父元件
import React, { useRef } from 'react'
import BaseInfo from './BaseInfo'

function RefTest() {
  const baseInfoRef = useRef(null)

  const getBaseInfo = () => {
    console.log('getBaseInfo --- baseInfoRef', baseInfoRef.current)
  }

  return (
    <>
      <BaseInfo
        dataSource={{ name: '混沌' }}
        ref={baseInfoRef}
      />
      <button onClick={getBaseInfo}>獲取基本資訊</button>
    </>
  )
}

輸出

image-20211122112817260.png
點選“獲取基本資訊”按鈕後:
image-20211122112901116.png

useImperativeHandle 和 forwardRef

useImperativeHandle 介紹

useImperativeHandle(ref, createHandle, [deps])

使用 ref 時通過useImperativeHandle自定義暴露給父元件的例項值。

通過 useImperativeHandle, 將父元件傳入的 ref 和 useImperativeHandle 第二個引數返回的物件繫結到了一起。

優點

  • 只暴露給父元件需要用到的 DOM 方法;
  • 在父元件中, 呼叫 xxxRef.current 時,返回的是物件;

例子

// 子元件
const OtherInfo = (props, ref) => {
  console.log('OtherInfo --- props', props)
  console.log('OtherInfo --- ref', ref)

  const inputRef = useRef()
  const [school, setSchool] = useState('')

  useImperativeHandle(ref, () => {
    console.log('useImperativeHandle --- ref', ref)
    return ({
      focus: () => {
        inputRef.current.focus()
      },
      getSchool: () => {
        return inputRef.current.value
      }
    })
  }, [inputRef])

  return (
    <div>
      學校:
      <input ref={inputRef} onChange={val => setSchool(val)}/>
    </div>
  )
}

export default forwardRef(OtherInfo)
// 父元件
import React, { useRef } from 'react'
import OtherInfo from './OtherInfo'

function RefTest() {
  const otherInfoRef = useRef(null)

  const getOtherInfo = () => {
    console.log('getOtherInfo --- otherInfoRef', otherInfoRef.current)
    console.log('getOtherInfo --- otherInfoRef --- getSchool', otherInfoRef.current.getSchool())
  }

  return (
    <>
      <OtherInfo
        dataSource={{ school: '大學' }}
        ref={otherInfoRef}
      />
      <button onClick={getOtherInfo}>獲取其他資訊</button>
    </>
  )
}

輸出
image-20211122140641074.png