你想知道的關(guān)于Refs的知識都在這了
Refs 提供了一種方式,允許我們訪問 DOM 節(jié)點或在 render 方法中創(chuàng)建的 React 元素。
Refs 使用場景
在某些情況下,我們需要在典型數(shù)據(jù)流之外強制修改子組件,被修改的子組件可能是一個 React 組件的實例,也可能是一個 DOM 元素,例如:
- 管理焦點,文本選擇或媒體播放。
- 觸發(fā)強制動畫。
- 集成第三方 DOM 庫。
設(shè)置 Refs
1. createRef
支持在函數(shù)組件和類組件內(nèi)部使用
createRef 是 React16.3 版本中引入的。
創(chuàng)建 Refs
使用 React.createRef() 創(chuàng)建 Refs,并通過 ref 屬性附加至 React 元素上。通常在構(gòu)造函數(shù)中,將 Refs 分配給實例屬性,以便在整個組件中引用。
訪問 Refs
當(dāng) ref 被傳遞給 render 中的元素時,對該節(jié)點的引用可以在 ref 的 current 屬性中訪問。
- import React from 'react';
- export default class MyInput extends React.Component {
- constructor(props) {
- super(props);
- //分配給實例屬性
- this.inputRef = React.createRef(null);
- }
- componentDidMount() {
- //通過 this.inputRef.current 獲取對該節(jié)點的引用
- this.inputRef && this.inputRef.current.focus();
- }
- render() {
- //把 <input> ref 關(guān)聯(lián)到構(gòu)造函數(shù)中創(chuàng)建的 `inputRef` 上
- return (
- <input type="text" ref={this.inputRef}/>
- )
- }
- }
ref 的值根據(jù)節(jié)點的類型而有所不同:
- 當(dāng) ref 屬性用于 HTML 元素時,構(gòu)造函數(shù)中使用 React.createRef() 創(chuàng)建的 ref 接收底層 DOM 元素作為其 current 屬性。
- 當(dāng) ref 屬性用于自定義的 class 組件時, ref 對象接收組件的掛載實例作為其 current 屬性。
- 不能在函數(shù)組件上使用 ref 屬性,因為函數(shù)組件沒有實例。
總結(jié):為 DOM 添加 ref,那么我們就可以通過 ref 獲取到對該DOM節(jié)點的引用。而給React組件添加 ref,那么我們可以通過 ref 獲取到該組件的實例【不能在函數(shù)組件上使用 ref 屬性,因為函數(shù)組件沒有實例】。
2. useRef
僅限于在函數(shù)組件內(nèi)使用
useRef 是 React16.8 中引入的,只能在函數(shù)組件中使用。
創(chuàng)建 Refs
使用 React.useRef() 創(chuàng)建 Refs,并通過 ref 屬性附加至 React 元素上。
- const refContainer = useRef(initialValue);
useRef 返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。
訪問 Refs
當(dāng) ref 被傳遞給 React 元素時,對該節(jié)點的引用可以在 ref 的 current 屬性中訪問。
- import React from 'react';
- export default function MyInput(props) {
- const inputRef = React.useRef(null);
- React.useEffect(() => {
- inputRef.current.focus();
- });
- return (
- <input type="text" ref={inputRef} />
- )
- }
關(guān)于 React.useRef() 返回的 ref 對象在組件的整個生命周期內(nèi)保持不變,我們來和 React.createRef() 來做一個對比,代碼如下:
- import React, { useRef, useEffect, createRef, useState } from 'react';
- function MyInput() {
- let [count, setCount] = useState(0);
- const myRef = createRef(null);
- const inputRef = useRef(null);
- //僅執(zhí)行一次
- useEffect(() => {
- inputRef.current.focus();
- window.myRef = myRef;
- window.inputRef = inputRef;
- }, []);
- useEffect(() => {
- //除了第一次為true, 其它每次都是 false 【createRef】
- console.log('myRef === window.myRef', myRef === window.myRef);
- //始終為true 【useRef】
- console.log('inputRef === window.inputRef', inputRef === window.inputRef);
- })
- return (
- <>
- <input type="text" ref={inputRef}/>
- <button onClick={() => setCount(count+1)}>{count}</button>
- </>
- )
- }
3. 回調(diào) Refs
支持在函數(shù)組件和類組件內(nèi)部使用
React 支持 回調(diào) refs 的方式設(shè)置 Refs。這種方式可以幫助我們更精細的控制何時 Refs 被設(shè)置和解除。
使用 回調(diào) refs 需要將回調(diào)函數(shù)傳遞給 React元素 的 ref 屬性。這個函數(shù)接受 React 組件實例 或 HTML DOM 元素作為參數(shù),將其掛載到實例屬性上,如下所示:
- import React from 'react';
- export default class MyInput extends React.Component {
- constructor(props) {
- super(props);
- this.inputRef = null;
- this.setTextInputRef = (ele) => {
- this.inputRef = ele;
- }
- }
- componentDidMount() {
- this.inputRef && this.inputRef.focus();
- }
- render() {
- return (
- <input type="text" ref={this.setTextInputRef}/>
- )
- }
- }
React 會在組件掛載時,調(diào)用 ref 回調(diào)函數(shù)并傳入 DOM元素(或React實例),當(dāng)卸載時調(diào)用它并傳入 null。在 componentDidMoune 或 componentDidUpdate 觸發(fā)前,React 會保證 Refs 一定是最新的。
可以在組件間傳遞回調(diào)形式的 refs.
- import React from 'react';
- export default function Form() {
- let ref = null;
- React.useEffect(() => {
- //ref 即是 MyInput 中的 input 節(jié)點
- ref.focus();
- }, [ref]);
- return (
- <>
- <MyInput inputRef={ele => ref = ele} />
- {/** other code */}
- </>
- )
- }
- function MyInput (props) {
- return (
- <input type="text" ref={props.inputRef}/>
- )
- }
4. 字符串 Refs(過時API)
函數(shù)組件內(nèi)部不支持使用 字符串 refs [支持 createRef | useRef | 回調(diào) Ref]
- function MyInput() {
- return (
- <>
- <input type='text' ref={'inputRef'} />
- </>
- )
- }
類組件
通過 this.refs.XXX 獲取 React 元素。
- class MyInput extends React.Component {
- componentDidMount() {
- this.refs.inputRef.focus();
- }
- render() {
- return (
- <input type='text' ref={'inputRef'} />
- )
- }
- }
Ref 傳遞
在 Hook 之前,高階組件(HOC) 和 render props 是 React 中復(fù)用組件邏輯的主要手段。
盡管高階組件的約定是將所有的 props 傳遞給被包裝組件,但是 refs 是不會被傳遞的,事實上, ref 并不是一個 prop,和 key 一樣,它由 React 專門處理。
這個問題可以通過 React.forwardRef (React 16.3中新增)來解決。在 React.forwardRef 之前,這個問題,我們可以通過給容器組件添加 forwardedRef (prop的名字自行確定,不過不能是 ref 或者是 key).
React.forwardRef 之前
- import React from 'react';
- import hoistNonReactStatic from 'hoist-non-react-statics';
- const withData = (WrappedComponent) => {
- class ProxyComponent extends React.Component {
- componentDidMount() {
- //code
- }
- //這里有個注意點就是使用時,我們需要知道這個組件是被包裝之后的組件
- //將ref值傳遞給 forwardedRef 的 prop
- render() {
- const {forwardedRef, ...remainingProps} = this.props;
- return (
- <WrappedComponent ref={forwardedRef} {...remainingProps}/>
- )
- }
- }
- //指定 displayName. 未復(fù)制靜態(tài)方法(重點不是為了講 HOC)
- ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
- //復(fù)制非 React 靜態(tài)方法
- hoistNonReactStatic(ProxyComponent, WrappedComponent);
- return ProxyComponent;
- }
這個示例中,我們將 ref 的屬性值通過 forwardedRef 的 prop,傳遞給被包裝的組件,使用:
- class MyInput extends React.Component {
- render() {
- return (
- <input type="text" {...this.props} />
- )
- }
- }
- MyInput = withData(MyInput);
- function Form(props) {
- const inputRef = React.useRef(null);
- React.useEffect(() => {
- console.log(inputRef.current)
- })
- //我們在使用 MyInput 時,需要區(qū)分其是否是包裝過的組件,以確定是指定 ref 還是 forwardedRef
- return (
- <MyInput forwardedRef={inputRef} />
- )
- }
React.forwardRef
Ref 轉(zhuǎn)發(fā)是一項將 ref 自動地通過組件傳遞到其一子組件的技巧,其允許某些組件接收 ref,并將其向下傳遞給子組件。
轉(zhuǎn)發(fā) ref 到DOM中:
- import React from 'react';
- const MyInput = React.forwardRef((props, ref) => {
- return (
- <input type="text" ref={ref} {...props} />
- )
- });
- function Form() {
- const inputRef = React.useRef(null);
- React.useEffect(() => {
- console.log(inputRef.current);//input節(jié)點
- })
- return (
- <MyInput ref={inputRef} />
- )
- }
- 調(diào)用 React.useRef 創(chuàng)建了一個 React ref 并將其賦值給 ref 變量。
- 指定 ref 為JSX屬性,并向下傳遞 <MyInput ref={inputRef}>
- React 傳遞 ref 給 forwardRef 內(nèi)函數(shù) (props, ref) => ... 作為其第二個參數(shù)。
- 向下轉(zhuǎn)發(fā)該 ref 參數(shù)到 <button ref={ref}>,將其指定為JSX屬性
- 當(dāng) ref 掛載完成,inputRef.current 指向 input DOM節(jié)點
注意
第二個參數(shù) ref 只在使用 React.forwardRef 定義組件時存在。常規(guī)函數(shù)和 class 組件不接收 ref 參數(shù),且 props 中也不存在 ref。
在 React.forwardRef 之前,我們?nèi)绻雮鬟f ref 屬性給子組件,需要區(qū)分出是否是被HOC包裝之后的組件,對使用來說,造成了一定的不便。我們來使用 React.forwardRef 重構(gòu)。
- import React from 'react';
- import hoistNonReactStatic from 'hoist-non-react-statics';
- function withData(WrappedComponent) {
- class ProxyComponent extends React.Component {
- componentDidMount() {
- //code
- }
- render() {
- const {forwardedRef, ...remainingProps} = this.props;
- return (
- <WrappedComponent ref={forwardedRef} {...remainingProps}/>
- )
- }
- }
- //我們在使用被withData包裝過的組件時,只需要傳 ref 即可
- const forwardRef = React.forwardRef((props, ref) => (
- <ProxyComponent {...props} forwardedRef={ref} />
- ));
- //指定 displayName.
- forwardRef.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
- return hoistNonReactStatic(forwardRef, WrappedComponent);
- }
- class MyInput extends React.Component {
- render() {
- return (
- <input type="text" {...this.props} />
- )
- }
- }
- MyInput.getName = function() {
- console.log('name');
- }
- MyInput = withData(MyInput);
- console.log(MyInput.getName); //測試靜態(tài)方法拷貝是否正常
- function Form(props) {
- const inputRef = React.useRef(null);
- React.useEffect(() => {
- console.log(inputRef.current);//被包裝組件MyInput
- })
- //在使用時,傳遞 ref 即可
- return (
- <MyInput ref={inputRef} />
- )
- }
react-redux 中獲取子組件(被包裝的木偶組件)的實例
舊版本中(V4 / V5)
我們知道,connect 有四個參數(shù),如果我們想要在父組件中子組件(木偶組件)的實例,那么需要設(shè)置第四個參數(shù) options 的 withRef 為 true。隨后可以在父組件中通過容器組件實例的 getWrappedInstance() 方法獲取到木偶組件(被包裝的組件)的實例,如下所示:
- //MyInput.js
- import React from 'react';
- import { connect } from 'react-redux';
- class MyInput extends React.Component {
- render() {
- return (
- <input type="text" />
- )
- }
- }
- export default connect(null, null, null, { withRef: true })(MyInput);
- //index.js
- import React from "react";
- import ReactDOM from "react-dom";
- import { createStore } from 'redux';
- import { Provider } from 'react-redux';
- import MyInput from './MyInput';
- function reducer(state, action) {
- return state;
- }
- const store = createStore(reducer);
- function Main() {
- let ref = React.createRef();
- React.useEffect(() => {
- console.log(ref.current.getWrappedInstance());
- })
- return (
- <Provider store={store}>
- <MyInput ref={ref} />
- </Provider>
- )
- }
- ReactDOM.render(<Main />, document.getElementById("root"));
這里需要注意的是:MyInput 必須是類組件,而函數(shù)組件沒有實例,自然也無法通過 ref 獲取其實例。react-redux 源碼中,通過給被包裝組件增加 ref 屬性,getWrappedInstance 返回的是該實例 this.refs.wrappedInstance。
- if (withRef) {
- this.renderedElement = createElement(WrappedComponent, {
- ...this.mergedProps,
- ref: 'wrappedInstance'
- })
- }
新版本(V6 / V7)
react-redux新版本中使用了 React.forwardRef方法進行了 ref 轉(zhuǎn)發(fā)。 自 V6 版本起,option 中的 withRef 已廢棄,如果想要獲取被包裝組件的實例,那么需要指定 connect 的第四個參數(shù) option 的 forwardRef 為 true,具體可見下面的示例:
- //MyInput.js 文件
- import React from 'react';
- import { connect } from 'react-redux';
- class MyInput extends React.Component {
- render() {
- return (
- <input type="text" />
- )
- }
- }
- export default connect(null, null, null, { forwardRef: true })(MyInput);
直接給被包裝過的組件增加 ref,即可以獲取到被包裝組件的實例,如下所示:
- //index.js
- import React from "react";
- import ReactDOM from "react-dom";
- import { createStore } from 'redux';
- import { Provider } from 'react-redux';
- import MyInput from './MyInput';
- function reducer(state, action) {
- return state;
- }
- const store = createStore(reducer);
- function Main() {
- let ref = React.createRef();
- React.useEffect(() => {
- console.log(ref.current);
- })
- return (
- <Provider store={store}>
- <MyInput ref={ref} />
- </Provider>
- )
- }
- ReactDOM.render(<Main />, document.getElementById("root"));
同樣,MyInput 必須是類組件,因為函數(shù)組件沒有實例,自然也無法通過 ref 獲取其實例。
react-redux 中將 ref 轉(zhuǎn)發(fā)至 Connect 組件中。通過 forwardedRef 傳遞給被包裝組件 WrappedComponent 的 ref。
- if (forwardRef) {
- const forwarded = React.forwardRef(function forwardConnectRef(
- props,
- ref
- ) {
- return <Connect {...props} forwardedRef={ref} />
- })
- forwarded.displayName = displayName
- forwarded.WrappedComponent = WrappedComponent
- return hoistStatics(forwarded, WrappedComponent)
- }
- //...
- const { forwardedRef, ...wrapperProps } = props
- const renderedWrappedComponent = useMemo(
- () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
- [forwardedRef, WrappedComponent, actualChildProps]
- )