useRef詳細總結

本文全方面介紹 useRef,分別從什麼是 useRef、爲何使用 useRef 以及 useRef 三兄弟(指 useRef 、 forwardRef 以及 useImperativeHandle )來說解。javascript

本文全部示例java

1、什麼是useRef

const refContainer = useRef(initialValue);
複製代碼
  • 返回一個可變的 ref 對象,該對象只有個 current 屬性,初始值爲傳入的參數( initialValue )。
  • 返回的 ref 對象在組件的整個生命週期內保持不變
  • 當更新 current 值時並不會 re-render ,這是與 useState 不一樣的地方
  • 更新 useRef 是 side effect (反作用),因此通常寫在 useEffect 或 event handler 裏
  • useRef 相似於類組件的 this

簡單示例

需求: 點擊 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
複製代碼

經過ref獲取組件內的DOM 小結: 經過useRef定義個inputEl變量,在input 元素上定義ref={inputEl},這樣經過inputEl.current就能夠獲取到input Dom 元素,選中則調用下focus函數便可web

見示例庫裏的domUseRef.tsx數組

2、爲何使用useRef

需求: 跨渲染取到狀態值markdown

只用useState:

實現: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值 爲何不是界面上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. 採用global變量 小結 因爲like變量是定義在組件外,因此不一樣渲染間是能夠共用該變量,因此3秒後獲取的like值就是最新的like值 該示例同時也說明,非state變量不會引發從新render

代碼見示例庫裏的globalFix1

採用useRef

實現:

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 小結 採用useRef,做爲組件實例的變量,保證獲取到的數據確定是最新的。

該示例同時也說明,ref更改不會re-render

見示例庫裏的useRefFix2

useRef與全局變量的區別

上面兩個法子均可以解決問題,那兩個有什麼區別呢

實現

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屬於組件本身,互相不影響)

小結

  • useRef 是定義在實例基礎上的,若是代碼中有多個相同的組件,每一個組件的 ref 只跟組件自己有關,跟其餘組件的 ref 沒有關係。
  • 組件前定義的 global 變量,是屬於全局的。若是代碼中有多個相同的組件,那這個 global 變量在全局是同一個,他們會互相影響。

代碼見示例庫裏的differenceFix1and2.tsx

3、useRef與createRef的區別

在一個組件的正常的生命週期中能夠大體分爲3個階段:

  1. 從建立組件到掛載到DOM階段。初始化props以及state, 根據state與props來構建DOM
  2. 組件依賴的props以及state狀態發生變動,觸發更新
  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,會被從新賦值,頁面就會顯示出新的值) useRef與createRef 小結: createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用

代碼見示例庫裏的useRefAndCreateRef

以上講的都是在當前組件使用ref的示例,後續接着說ref如何用在與子組件的交互上

4、獲取子組件的屬性或方法

需求: 調用子組件裏的某個函數

自定義屬性傳入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做爲屬性傳入子組件 小結: 不夠優雅,尤爲是自定義一個屬性傳入ref

見示例庫裏的childComponentRef.tsx

經過useImperativeHandle,配合forwardRef

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

複製代碼

現象:

只往外暴露getValue 小結: React 16.3版本以前,是不可以在函數組件裏定義ref屬性,由於函數組件沒有實例。在16.3版本以後,引入了React.forwardRef,經過該函數就能夠將父組件的ref傳入子組件,子組件能夠將該ref綁定在任何DOM元素上,但這樣會將整個子組件暴露給父組件。經過useImperativeHandle就能夠限制要將子組件裏的哪些屬性或方法暴露給父組件

注意: 使用 antd 的 Form.create() 包裝子組件以後,在父組件中要將 ref 屬性改爲 wrappedComponentRef <ChildInput label={'名稱'} wrappedComponentRef={childRef} />

見示例庫裏的childComponentRef2

5、總結

  1. useRef能夠用來定義變量,這些變量更改以後不會引發頁面從新渲染,好比分頁獲取數據時,存儲頁碼。
  2. useRef也能夠用來區分初始渲染仍是更新(經過current有沒值,具體見示例庫裏的didOrUpdate.tsx)
  3. 在DOM節點上定義ref屬性,經過.current就能夠獲取到該DOM元素
  4. 經過forwardRef就能夠給函數子組件傳入ref屬性。
  5. 使用useImperativeHandle用於定義暴露給父組件的ref方法
相關文章
相關標籤/搜索