React高手都善于使用useImprativeHandle
一、useRef
學習 useImperativeHandle,得從 useRef 說起。我們前面已經學習過了 useRef,它能夠結合元素組件的 ref 屬性幫我們拿到該元素組件對應的真實 DOM。
例如,我想要拿到一個 input 元素的真實 DOM 對象,并調用 input 的 .focus() 方法,讓 input 獲得焦點。
import {useRef} from "react";
export default function Demo() {
const inputRef = useRef<HTMLInputElement>(null);
const focusTextInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={focusTextInput}>
點擊我讓input組件獲得焦點
</button>
</>
);
}
每一個 React 提供的元素組件,都具備 ref 屬性。在上面的章節中我們可以知道,當我們拿到了元素的原生 DOM 對象之后,就可以脫離 React 的開發思路,從而應對更多更復雜的場景。
那么問題就來了,原生組件有自己的 ref 屬性,那么自定義組件呢?當然是沒有的,因此我們得自己想辦法處理。
二、forwardRef
forwardRef 能夠在我們自定義組件時,把內部組件的 ref 屬性傳遞給父組件。
它接受我們自定義的組件作為參數,并返回一個新的組件。新組件具備我們自定義組件的全部能力,并得到一個 ref 屬性,父組件通過 useRef 獲取到的內容與內部組件的 ref 完全一致。
我們來看一個案例。
現在我們要實現如下效果,當點擊 Edit 按鈕時,輸入框自動獲得焦點。
我們知道,在 DOM 中,只要得到 input 對象,然后就可以調用 .focus() 方法來實現目標?,F在我們要封裝一個自定義的 MyInput 組件,他具備 input 同樣的能力,同時,我們還要封裝一個標題進去。
<label>Enter your name</label>
<input />
我們的代碼如下:
import {forwardRef, LegacyRef} from 'react'
type MyInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string
}
function MyInput(props: MyInputProps, ref: LegacyRef<HTMLInputElement>) {
const {label, ...other} = props
return (
<label>
{label}
<input {...other} ref={ref} />
</label>
)
}
export default forwardRef(MyInput)
MyInput 在聲明時要傳入兩個參數,一個 props,一個 ref。通過展開運算符,我們能夠確保 MyInput 支持 input 所有的屬性。
封裝好之后,我們就可以在點擊實踐中,通過 ref 得到的引用去調用 .focus() 達到 input 獲取焦點的目標。
import { useRef } from 'react'
import MyInput from './MyInput'
export default function ImperativeHandle() {
const ref = useRef<any>(null)
function handleClick() {
ref.current?.focus()
}
return (
<form>
<MyInput
label='Enter your name:'
ref={ref}
/>
<button type='button' onClick={handleClick}>Edit</button>
</form>
)
}
三、useImperativeHandle
在實踐中,很多時候,我們并不想通過 ref 去獲取子組件內部的某個元素組件的真實 DOM 對象。而是希望父組件能夠調用子組件內部的某些方法
但是在 React 中,又無法直接 new 一個子組件的實例,像面向對象那樣通過子組件實例去調用子組件的方法。
因此,React 提供了一個 hook,useImperativeHandle,讓我們能夠重寫子組件內部 ref 對應的引用,從而達到在父組件中,調用子組件內部方法的目的
例如,上面的 MyInput 組件,我們可以修改代碼為:
import {forwardRef, useImperativeHandle, useRef} from 'react'
type MyInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string
}
function MyInput(props: MyInputProps, ref: any) {
const {label, ...other} = props
const inputRef = useRef<any>(null)
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus()
}
}
}, [])
return (
<label>
{label}
<input {...other} ref={inputRef} />
</label>
)
}
export default forwardRef(MyInput)
useImperativeHandle(
ref,
createHandle,
dependencies?
)
useImperativeHandle 接收三個參數,分別是
- ref: 組件聲明時傳入的 ref。
- createHandle: 回調函數,需要返回 ref 引用的對象,我們也是在這里重寫 ref 引用。
- deps: 依賴項數組,可選。state,props 以及內部定義的其他變量都可以作為依賴項,React 內部會使用 Object.is 來對比依賴項是否發生了變化。依賴項發生變化時,createHandle 會重新執行,ref 引用會更新。如果不傳入依賴項,那么每次更新 createHandle 都會重新執行。
useImperativeHandle 執行本身返回 undefined。
四、官方案例
官方文檔中有這種一個案例,效果如圖所示。當點擊按鈕時,我希望下方的 input 自動獲得焦點,并切中間的滾動條滾動到最底部。
現在,我們結合前面的知識來分析一下這個案例應該如何實現。
首先我們先進行組件拆分,將整個內容拆分為按鈕部分與信息部分,信息部分主要負責信息的暫時與輸入,因此頁面組件大概長這樣。
<>
<button>Write a comment</button>
<Post />
</>
我們期望點擊按鈕時,信息部分的輸入框自動獲取焦點,信息部分的信息展示區域能滾動到最底部,因此整個頁面組件的代碼可以表示為如下:
import { useRef } from 'react';
import Post from './Post.js';
export default function Page() {
const postRef = useRef(null);
function handleClick() {
postRef.current.scrollAndFocusAddComment();
}
return (
<>
<button onClick={handleClick}>
Write a comment
</button>
<Post ref={postRef} />
</>
);
}
信息部分 Post 又分為兩個部分,分別是信息展示部分與信息輸入部分。
此時這兩個部分的 ref 要透傳給 Post,并最終再次透傳給頁面組件。
所以信息展示部分 CommentList 組件的代碼為。
import { forwardRef, useRef, useImperativeHandle } from 'react';
const CommentList = forwardRef(function CommentList(props, ref) {
const divRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollToBottom() {
const node = divRef.current;
node.scrollTop = node.scrollHeight;
}
};
}, []);
let comments = [];
for (let i = 0; i < 50; i++) {
comments.push(<p key={i}>Comment #{i}</p>);
}
return (
<div className="CommentList" ref={divRef}>
{comments}
</div>
);
});
export default CommentList;
信息輸入部分 AddComment 的代碼為。
import { forwardRef, useRef, useImperativeHandle } from 'react';
const AddComment = forwardRef(function AddComment(props, ref) {
return <input placeholder="Add comment..." ref={ref} />;
});
export default AddComment;
Post 要把他們整合起來。
import { forwardRef, useRef, useImperativeHandle } from 'react';
import CommentList from './CommentList.js';
import AddComment from './AddComment.js';
const Post = forwardRef((props, ref) => {
const commentsRef = useRef(null);
const addCommentRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollAndFocusAddComment() {
commentsRef.current.scrollToBottom();
addCommentRef.current.focus();
}
};
}, []);
return (
<>
<article>
<p>Welcome to my blog!</p>
</article>
<CommentList ref={commentsRef} />
<AddComment ref={addCommentRef} />
</>
);
});
export default Post;
這樣,我們整個案例的代碼就寫完了。useRef、useImprativeHandle、forwardRef 一起配合幫助我們完成了這個功能。