本文全方面介紹 useRef,分別從什麼是 useRef、爲何使用 useRef 以及 useRef 三兄弟(指 useRef 、 forwardRef 以及 useImperativeHandle )來說解。javascript
本文全部示例java
const refContainer = useRef(initialValue);
複製代碼
需求: 點擊 button 的時候 選中文本框node
實現:react
import React, { MutableRefObject, useRef } from 'react'
const TextInputWithFocusButton: React.FC = () => {
const inputEl: MutableRefObject<any> = useRef(null)
const handleFocus = () => {
// `current` 指向已掛載到 DOM 上的文本輸入元素
inputEl.current.focus()
}
return (
<p> <input ref={inputEl} type="text" /> <button onClick={handleFocus}>Focus the input</button> </p>
)
}
export default TextInputWithFocusButton
複製代碼
小結: 經過useRef定義個inputEl變量,在input 元素上定義ref={inputEl},這樣經過inputEl.current就能夠獲取到input Dom 元素,選中則調用下focus函數便可web
見示例庫裏的domUseRef.tsx數組
需求: 跨渲染取到狀態值markdown
實現:antd
import React, { useState } from "react";
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`)
//造成閉包,因此彈出來的是當時觸發函數時的like值
}, 3000)
}
return (
<> <button onClick={() => setLike(like + 1)}>{like}贊</button> <button onClick={handleAlertClick}>Alert</button> </>
)
}
export default LikeButton
複製代碼
現象: 在like爲6的時候, 點擊 alert , 再繼續增長like到10, 彈出的值爲 6, 而非 10. 爲何不是界面上like的實時狀態? 當咱們更改狀態的時候,React會從新渲染組件,每次的渲染都會拿到獨立的like值,並從新定義個handleAlertClick函數,每一個handleAlertClick函數體裏的like值也是它本身的,因此當like爲6時,點擊alert,觸發了handleAlertClick,此時的like是6,哪怕後面繼續更改like到10,但alert時的like已經定下來了。閉包
小結: 不一樣渲染之間沒法共享state狀態值app
見示例庫裏的likeButton
在組件前定義一個相似 global 的變量
實現:
import React from "react";
let like = 0;
const LikeButton: React.FC = () => {
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`);
}, 3000);
}
return (
<> <button onClick={() => { like = ++like; }} > {like}贊 </button> <button onClick={handleAlertClick}>Alert</button> </>
);
};
export default LikeButton;
複製代碼
現象 在like爲6的時候, 點擊 alert , 再繼續增長like到10, 彈出的值爲10. 小結 因爲like變量是定義在組件外,因此不一樣渲染間是能夠共用該變量,因此3秒後獲取的like值就是最新的like值 該示例同時也說明,非state變量不會引發從新render
代碼見示例庫裏的globalFix1
實現:
import React, { useRef } from "react";
const LikeButton: React.FC = () => {
// 定義一個實例變量
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<> <button onClick={() => { like.current = like.current + 1; }} > {like.current}贊 </button> <button onClick={handleAlertClick}>Alert</button> </>
);
};
export default LikeButton;
複製代碼
現象 在like爲6的時候, 點擊 alert , 再繼續增長like到10, 彈出的值爲10.跟上面使用全局變量現象一致 小結 採用useRef,做爲組件實例的變量,保證獲取到的數據確定是最新的。
該示例同時也說明,ref更改不會re-render
見示例庫裏的useRefFix2
上面兩個法子均可以解決問題,那兩個有什麼區別呢
實現
import React, { useRef } from "react";
// 定義一個全局變量
let like = 0;
const LikeButton: React.FC = () => {
let likeRef = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`);
alert(`you clicked on ${likeRef.current}`);
}, 3000);
}
return (
<p> <button onClick={() => { like = ++like; likeRef.current = likeRef.current + 1; }} > 點贊 </button> <button onClick={handleAlertClick}>Alert</button> </p>
);
};
export default LikeButton;
複製代碼
現象 三個按鈕依次點下,點擊任意alert,最早彈出的是3(表示global變量取的最後渲染組件的值),後彈出1(表示ref屬於組件本身,互相不影響)
小結
代碼見示例庫裏的differenceFix1and2.tsx
在一個組件的正常的生命週期中能夠大體分爲3個階段:
第一個階段,useRef與createRef沒有差異
第二個階段,createRef每次都會返回個新的引用;而useRef不會隨着組件的更新而從新建立
第三個階段,二者都會銷燬
實現:
import React, { useState, useRef, createRef } from 'react'
const RefDifference: React.FC = () => {
let [renderIndex, setRenderIndex] = useState(1)
let refFromUseRef = useRef<number>()
let refFromCreateRef = createRef()
console.info(refFromUseRef.current, 'refFromUseRef.current')
console.info(refFromCreateRef.current, 'refFromCreateRef.current')
if (!refFromUseRef.current) {
refFromUseRef.current = renderIndex
}
if (!refFromCreateRef.current) {
refFromCreateRef.current = renderIndex
}
return (
<> <p>Current render index: {renderIndex}</p> <p> <b>refFromUseRef</b> value: {refFromUseRef.current} </p> <p> <b>refFromCreateRef</b> value: {refFromCreateRef.current} </p> <button onClick={() => setRenderIndex((prev) => prev + 1)}> Cause re-render </button> </>
)
}
export default RefDifference
複製代碼
現象: 點擊按鈕時,從控制檯能夠看到refFromUseRef.current一直爲1(由於refFromUseRef.current已經存在該引用),而refFromCreateRef.current倒是undefined(由於createRef 每次渲染都會返回一個新的引用,因此if判斷時爲true,會被從新賦值,頁面就會顯示出新的值) 小結: createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用
代碼見示例庫裏的useRefAndCreateRef
以上講的都是在當前組件使用ref的示例,後續接着說ref如何用在與子組件的交互上
需求: 調用子組件裏的某個函數
實現: 父組件建立一個ref做爲一個屬性傳入子組件。子組件根據內部方法的變化動態更改ref(useEffect)
import React, {
MutableRefObject,
useState,
useEffect,
useRef,
useCallback
} from 'react'
interface IProps {
//prettier-ignore
label: string,
cRef: MutableRefObject<any>
}
const ChildInput: React.FC<IProps> = (props) => {
const { label, cRef } = props
const [value, setValue] = useState('')
const handleChange = (e: any) => {
const value = e.target.value
setValue(value)
}
const getValue = useCallback(() => {
return value
}, [value])
useEffect(() => {
if (cRef && cRef.current) {
cRef.current.getValue = getValue
}
}, [getValue])
return (
<div> <span>{label}:</span> <input type="text" value={value} onChange={handleChange} /> </div>
)
}
const ParentCom: React.FC = (props: any) => {
const childRef: MutableRefObject<any> = useRef({})
const handleFocus = () => {
const node = childRef.current
alert(node.getValue())
}
return (
<div> <ChildInput label={'名稱'} cRef={childRef} /> <button onClick={handleFocus}>focus</button> </div>
)
}
export default ParentCom
複製代碼
現象: 父組件按鈕點擊時,經過調用getValue,獲取到子組件input裏的value值 小結: 不夠優雅,尤爲是自定義一個屬性傳入ref
見示例庫裏的childComponentRef.tsx
forwardRef: 將父類的ref做爲參數傳入函數式組件中
示例:
React.forwardRef((props, ref) => {})
//建立一個React組件,
//這個組件將會接受到父級傳遞的ref屬性,
//能夠將父組件建立的ref掛到子組件的某個dom元素上,
//在父組件經過該ref就能獲取到該dom元素
複製代碼
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton"> {props.children} </button>
));
// 能夠直接獲取到button的DOM節點
const ref = React.useRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
複製代碼
**useImperativeHandle:**在函數式組件中,用於定義暴露給父組件的ref方法,用來限制子組件對外暴露的信息,只有useImperativeHandle第二個參數定義的屬性跟方法才能夠在父組件獲取到
爲何使用: 由於使用forward+useRef獲取子函數式組件DOM時,獲取到的dom屬性暴露的太多了
解決: 使用 uesImperativeHandle 解決,在子函數式組件中定義父組件須要進行的 DOM 操做,沒有定義的就不會暴露給父組件
useImperativeHandle(ref, createHandle, [deps]) // 第一個參數暴露哪一個ref;第二個參數暴露什麼信息
複製代碼
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
// 渲染 <FancyInput ref={inputRef} /> 的父組件
// 能夠調用 inputRef.current.focus()
複製代碼
實現:
import React, {
MutableRefObject,
useState,
useImperativeHandle,
useRef,
forwardRef,
useCallback
} from 'react'
interface IProps {
label: string
}
let ChildInput = forwardRef((props: IProps, ref: any) => {
const { label } = props
const [value, setValue] = useState('')
// 做用: 減小父組件獲取的DOM元素屬性,只暴露給父組件須要用到的DOM方法
// 參數1: 父組件傳遞的ref屬性
// 參數2: 返回一個對象,父組件經過ref.current調用對象中方法
useImperativeHandle(ref, () => ({
getValue
}))
const handleChange = (e: any) => {
const value = e.target.value
setValue(value)
}
const getValue = useCallback(() => {
return value
}, [value])
return (
<div> <span>{label}:</span> <input type="text" value={value} onChange={handleChange} /> </div>
)
})
const ParentCom: React.FC = (props: any) => {
const childRef: MutableRefObject<any> = useRef({})
const handleFocus = () => {
const node = childRef.current
alert(node.getValue())
}
return (
<div> <ChildInput label={'名稱'} ref={childRef} /> <button onClick={handleFocus}>focus</button> </div>
)
}
export default ParentCom
複製代碼
現象:
小結: React 16.3版本以前,是不可以在函數組件裏定義ref屬性,由於函數組件沒有實例。在16.3版本以後,引入了React.forwardRef,經過該函數就能夠將父組件的ref傳入子組件,子組件能夠將該ref綁定在任何DOM元素上,但這樣會將整個子組件暴露給父組件。經過useImperativeHandle就能夠限制要將子組件裏的哪些屬性或方法暴露給父組件
注意: 使用 antd 的 Form.create()
包裝子組件以後,在父組件中要將 ref 屬性改爲 wrappedComponentRef <ChildInput label={'名稱'} wrappedComponentRef={childRef} />
見示例庫裏的childComponentRef2