在典型的 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>
</>
)
}
輸出
點選“獲取基本資訊”按鈕後:
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>
</>
)
}
輸出