React中ref是一個對象,它有一個current屬性,可以對這個屬性進行操作,用於獲取DOM元素和保存變化的值。什么是保存變化的值?就是在組件中,你想保存與組件渲染無關的值,就是JSX中用不到的或不顯示到頁面上的值,比如setTimeout的返回的ID,就可以把這個值放到ref中。為什么要放到ref中,因為更改ref的值,不會引起組件的重新渲染,因為值與渲染無關,它也不應該引起組件渲染。怎么獲取ref對象呢?調用useRef()函數和createRef()函數,它們返回ref對象。在組件的整個生命周期中,ref對象一直存在。組件創建,更准確地說法是,組件掛載,ref對象創建,組件銷毀,ref對象銷毀。
useRef是一個React Hooks,在函數組件中使用,它還可以接受一個參數,用於初始化useRef返回的對象的current屬性。
const ref = useRef(initialValue);
使用useRef獲取DOM元素,就是把ref對象賦給react element的ref屬性, 每一個react element都有一個ref屬性。組件掛載后,ref對象的current屬性,就自動指向DOM元素
import React, { useRef } from "react"; const CustomTextInput = () => { const textInput = useRef(); const focusTextInput = () => textInput.current.focus(); return ( <> <input type="text" ref={textInput} /> <button onClick={focusTextInput}>Focus the text input</button> </> ); }
組件掛載完成,textInput.current指向input輸入框,就可以直接調用輸入框的focus方法。
使用useRef保存變量的值,直接把變量或值,賦值給ref對象的current屬性就可以了。
import React, { useRef, useEffect } from "react"; const Timer = () => { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => { console.log("A second has passed"); }, 1000); intervalRef.current = id; return () => clearInterval(intervalRef.current); }); const handleCancel = () => clearInterval(intervalRef.current); return ( <> //... </> ); }
這里要注意的是更新ref對象的值,是一個side effect,因為這個值不參與渲染,更新值是React渲染之外,要做的事情,所以要放到useEffect或useLayoutEffect中,放到事件處理函數中也可以。如果以下有代碼
import React, { useRef } from "react"; const RenderCounter = () => { const counter = useRef(0); counter.current = counter.current + 1; return ( <h1>{`The component has been re-rendered ${counter} times`}</h1> ); };
最好改成
import React, { useRef } from "react"; const RenderCounter = () => { const counter = useRef(0); useEffect(() => { counter.current = counter.current + 1; }); return ( <h1>{`The component has been re-rendered ${counter} times`}</h1> ); };
函數組件中也可以使用createRef, 但當使用createRef時,每一次組件渲染時都會創建全新的ref對象,而不是每一次渲染都共用一個ref對象,性能會有問題,再說useRef就是代替createRef的,所以在函數組件中就沒有必要使用createRef了。
其實,使用useRef,也可以獲取到子組件,直接調用子組件中的方法,不過就是用點麻煩,因為ref只能獲取到類組件的實例,也只有類才有實例。函數組件是沒有實例的,怎么獲取到它?使用forwardref, 把一個函數組件包起來,函數組件就多了一個ref屬性。子組件中用useImperativeHandle暴露方法。結合forwardRef 和useImperativeHandle。使用create-react-app 創建React項目,在src中創建一個Counter組件
import React from 'react'; import { useState } from "react" const Counter = () => { const [count, setCount] = useState(0); const clickHandler = () => { setCount(c => c + 1); } return ( <p>count is {count} </p> ) } export default Counter;
然后在App.js中引入
import React from 'react'; import Counter from "./Counter"; function App() { return ( <React.Fragment> <Counter></Counter> <button>Add</button> </React.Fragment> ); } export default App;
此時,如果想點擊父組件App中的button來增加子組件的count,怎么辦?首先,子組件Counter,要把clickHandler方法暴露出來。做法,1,export的不是組件了,而是forwardRef(組件); 2,組件要接受參數ref,const Counter = (props, ref); 3, 在組件內部,使用useImperativeHandle,它的第一個參數是ref,第二個參數是回調函數,返回一個對象,對象中的屬性和方法,就可以在父組件中使用ref獲取到。
import React, { forwardRef, useImperativeHandle } from 'react'; import { useState } from "react" // 組件被forwardRef之后,組件多了一個ref屬性 const Counter = (props, ref) => { const [count, setCount] = useState(0); const clickHandler = () => { setCount(c => c + 1); } // 第一個參數就是ref,暴露出click方法,供父組件使用 useImperativeHandle(ref, () => { return ({ click: clickHandler }) }) return ( <p>count is {count} </p> ) } // export forwardRef(組件) export default forwardRef(Counter);
其次,在父組件App中使用ref,引用子組件,並在button的click回調函數中使用ref
function App() { const counteRef = useRef(); const handleClick = () => { counteRef.current.click(); } return ( <React.Fragment> <Counter ref={counteRef}></Counter> <button onClick={handleClick}>Add</button> </React.Fragment> ); }
如果項目中使用了Redux和React-Redux,子組件export的是connect()(組件),是一個高階組件,那父組件中怎么引用子組件呢?如果是react-redux 6.0以前的版本,connect函數第四個參數設置為{ withRef: true },父組件getWrappedInstance()就可以獲取到包裹的子組件
connect(null, null, null, { withRef: true })(組件);
如果是react-redux 6.0 以后的版本,使用 forwardRef: true , 代替{ withRef: true },父組件中的ref可以直接獲取到包包裹的子組件
connect(null, null, null, { forwardRef: true })(組件);