轉載請注明出處原文鏈接地址 Vincent'Blog-React中Refs的使用方法
什么是Refs
Refs 提供了一種方式,允許我們訪問 DOM 節點或在 render 方法中創建的 React 元素。
Ref轉發是一項將ref自動通過組件傳遞到子組件的技巧。 通常用來獲取DOM節點或者React元素實例的工具。在React中Refs提供了一種方式,允許用戶訪問dom節點或者在render方法中創建的React元素。
Refs轉發
Ref 轉發是一個可選特性,其允許某些組件接收 ref,並將其向下傳遞(換句話說,“轉發”它)給子組件。
背景
在React單向數據流中,props是父子組件交互的唯一方式。要修改一個子組件,需要通過新的props來重新渲染。在有些情況下,需要在數據流之外強行修改子組件(組件或者Dom元素),那么可以通過Refs來進行修改子組件。
組件類型
受控組件
在 HTML 中,表單元素(如
<input>
、<textarea>
和<select>
)之類的表單元素通常自己維護 state,並根據用戶輸入進行更新。而在 React 中,可變狀態(mutable state)通常保存在組件的 state 屬性中,並且只能通過使用 setState()來更新。我們可以把兩者結合起來,使 React 的 state 成為“唯一數據源”。渲染表單的 React 組件還控制着用戶輸入過程中表單發生的操作。被 React 以這種方式控制取值的表單輸入元素就叫做“受控組件”。
非受控組件
不被React組件控制的組件。在受控制組件中,表單數據由 React組件自行處理。其中表單數據由DOM本身處理。文件輸入標簽就是一個典型的不受控制組件,它的值只能由用戶設置,通過DOM自身提供的一些特性來獲取。
受控組件與非受控組件的區別
受控組件和不受控組件最大的區別就是前者自身維護的狀態值變化,可以配合自身的change事件,很容易進行修改或者校驗用戶的輸入。在React中 因為 Refs的出現使得 不受控制組件自身狀態值的維護變得容易了許多。
使用場景
- 對Dom元素的焦點控制、內容選擇、控制
- 對Dom元素的內容設置及媒體播放
- 對Dom元素的操作和對組件實例的操作
- 集成第三方 DOM 庫
避免使用 refs 來做任何可以通過聲明式實現來完成的事情。
舉個例子,避免在 Dialog 組件里暴露 open() 和 close() 方法,最好傳遞 isOpen 屬性。
訪問 Refs
當 ref 被傳遞給 render 中的元素時,對該節點的引用可以在 ref 的 current 屬性中被訪問。
const node = this.myRef.current;
ref 的值根據節點的類型而有所不同:
- 當 ref 屬性用於 HTML 元素時,構造函數中使用 React.createRef() 創建的 ref 接收底層 DOM 元素作為其 current 屬性。
- 當 ref 屬性用於自定義 class 組件時,ref 對象接收組件的掛載實例作為其 current 屬性。
- 不能再函數組件上使用Ref屬性,因為函數組件沒有實例。
例子
創建 Refs
Refs 是使用 React.createRef() 創建的,並通過 ref 屬性附加到 React 元素。在構造組件時,通常將 Refs 分配給實例屬性,以便可以在整個組件中引用它們。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div />;
}
}
DOM為元素添加ref
用 ref 去存儲 DOM 節點的引用:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 創建一個 ref 來存儲 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 輸入框獲得焦點
// 注意:我們通過 "current" 來訪問 DOM 節點
this.textInput.current.focus();
}
render() {
// 告訴 React 我們想把 <input> ref 關聯到
// 構造器里創建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
組件會在掛在是給current屬性傳入DOM元素,並在組件卸載時傳入null。ref會在componentDidMount或者componentDidUpdate生命周期觸發前更新。
class組件添加ref
包裝上面的 CustomTextInput,來模擬它掛載之后立即被點擊的操作,我們可以使用 ref 來獲取這個自定義的 input 組件並手動調用它的 focusTextInput 方法:
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
函數組件添加refs
默認情況下,不能再函數組件上使用ref屬性,因為函數組件沒有實例。如果要在函數組件上使用ref,需要使用useRef
和useImperativeHandle
結合使用。
useRef 返回一個可變的 ref 對象,其 current 屬性被初始化為傳入的參數(initialValue)
useRef 返回的 ref 對象在組件的整個生命周期內保持不變,也就是說每次重新渲染函數組件時,返回的ref 對象都是同一個(使用 React.createRef ,每次重新渲染組件都會重新創建 ref)
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
<Child />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
</>
)
}
let input;
function Child() {
const inputRef = useRef();
console.log('input===inputRef', input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>獲得焦點</button>
</>
)
}
forwardRef.
- 因為函數組件沒有實例,所以函數組件無法像類組件一樣可以接收 ref 屬性
- forwardRef 可以在父組件中操作子組件的 ref 對象
- forwardRef 可以將父組件中的 ref 對象轉發到子組件中的 dom 元素上
- 子組件接受 props 和 ref 作為參數
function Child(props,ref){
return (
<input type="text" ref={ref}/>
)
}
Child = React.forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
// 在使用類組件的時候,創建 ref 返回一個對象,該對象的 current 屬性值為空
// 只有當它被賦給某個元素的 ref 屬性時,才會有值
// 所以父組件(類組件)創建一個 ref 對象,然后傳遞給子組件(類組件),子組件內部有元素使用了
// 那么父組件就可以操作子組件中的某個元素
// 但是函數組件無法接收 ref 屬性 <Child ref={xxx} /> 這樣是不行的
// 所以就需要用到 forwardRef 進行轉發
const inputRef = useRef();//{current:''}
function getFocus(){
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>獲得焦點</button>
</>
)
}
useImperativeHandle
- useImperativeHandle可以讓你在使用 ref 時,自定義暴露給父組件的實例值
- 在大多數情況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應當與 forwardRef 一起使用
- 父組件可以使用操作子組件中的多個 ref
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';
function Child(props,parentRef){
// 子組件內部自己創建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>(
// 這個函數會返回一個對象
// 該對象會作為父組件 current 屬性的值
// 通過這種方式,父組件可以使用操作子組件中的多個 ref
return {
focusRef,
inputRef,
name:'計數器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
});
return (
<>
<input ref={focusRef}/>
<input ref={inputRef}/>
</>
)
}
Child = forwardRef(Child);
function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因為子組件中沒有定義這個屬性,實現了保護,所以這里的代碼無效
parentRef.current.addNumber(666);
parentRef.current.changeText('<script>alert(1)</script>');
console.log(parentRef.current.name);
}
return (
<>
<ForwardChild ref={parentRef}/>
<button onClick={getFocus}>獲得焦點</button>
</>
)
}