來自 React 19 的背刺:ForwardRef 被無情拋棄
在之前的版本中,forwardRef 一直是我最愛用的 ref 之一。它在封裝組件時非常有用。可是萬萬沒想到,由于使用方式稍微麻煩了一點,在新的版本中,直接被 React 19 背刺一刀,實現同樣的功能,以后可以不用它了.... forwardRef 被無情拋棄。
本文主要內容包括如下幾個部分。
- React 中的 控制反轉 IOC。
- forwardRef 基礎知識。
- React 19 中 ref 機制更改,forwardRef 被無情拋棄。
- useImperativeHandle 與 ref 的新配合。
一、React 中的控制反轉
在面向對象編程中,IOC (Inversion of Control) 控制反轉是一個非常高級的概念。它是一種設計理念,在減少對象之間的耦合關系上有非常重要的作用。
?
許多前端雖然對其有所耳聞,但說實話,能理解的并不多。甚至很對前端對解耦這個概念都是一頭霧水。
IOC 的設計理念里,有三個角色,一個角色是容器 C,一個角色是被控制者 B,一個角色是控制者 A,許多時候,在代碼開發中,控制者 A 直接去控制對象 B,會導致 B 被多次實例化而從讓代碼邏輯變得更加復雜。因此 IOC 的理念是讓 控制者 A 失去對 B 的直接控制權,它只能與容器交互。從而將 A 與 B 的直接聯系隔離開。
這樣說可能會有點繞,但是呢,我們使用一個大家經常使用的代碼來說明一下,你一下就能明白。
const ref = useRef()
<InputNumber ref={ref} />
ref.current.focus()
在這個案例里,我們的目標是直接獲取到 input 對象,并且調用它的 focus 方法,讓 input 獲得焦點。
var input = document.getElementById('input')
input.focus()
但是在模塊的劃分上,input 元素本身并不屬于當前模塊/組件,因此,調用 input 方法的行為,其實是屬于一種混亂。除非我們不做解耦和封裝。
因此,在 React 的組件封裝中,并不支持直接獲取到 input 的引用,而是以一種傳入控制器的方式來調用它。在這個場景里:
input 對象本身是被調用者
InputNumber 組件是容器
ref 是控制器
當前組件利用 ref 來調用 input。從而讓代碼的解耦變得非常合理。可擴展性也很強。
?
注意一些概念上的區分:控制反轉是一種設計思維,依賴注入是控制反轉的一種具體實現,在 React 中,ref 也是一種控制反轉的具體實現。
所以不要聽著別人吹控制反轉就覺得牛,你可能也天天在用。
二、forwardRef 基礎知識
forwardRef 能夠幫助 React 組件傳遞 ref。或者說是內部對象控制權的轉移與轉發。它接收一個組件作為參數。
import { forwardRef } from 'react';
function MyInput(props, ref) {
// ...
}
const InputNumber = forwardRef(MyInput);
這里需要注意的是,我們需要把 ref 放在自定義組件的參數中。
function MyInput(props, ref) {
// ...
}
forwardRef 返回一個可渲染的函數組件。因此,我們可以通過一個簡單的案例完善上面的代碼。
function MyInput(props, ref) {
return (
<label>
{props.label}
<input ref={ref} />
</label>
);
}
const InputNumber = forwardRef(MyInput)
所以呢,在 React 的開發中,forwardRef 能夠幫助我們實現更良好的解耦,這也是我一直非常喜歡使用 forwardRef 的原因。
三、ref 機制更改,forwardRef 被無情拋棄
但是,在 React 19 中,forwardRef 被直接背刺,由于 ref 傳遞機制的更改,我們可以不用 forwardRef 也能做到同樣的事情了。
首先,在聲明組件時,ref 不再獨立成為一個參數,而是作為 props 屬性中的一個屬性。
function MyInput(props) {
return (
<label>
{props.label}
<input ref={props.ref} />
</label>
);
}
其次,代碼這樣寫了之后,就可以直接在父組件中,通過 ref 拿到 input 的控制權。
function Index() {
const input = useRef(null)
function __clickHandler() {
input.current.focus()
}
return (
<div>
<button onClick={__clickHandler}>
點擊獲取焦點
</button>
<MyInput ref={input} />
</div>
)
}
在父組件中的使用與以前一樣,但是子組件由于不再需要 forwardRef,變得更加簡單了。
四、useImperativeHandle 與 ref 的新配合
除了直接拿到元素對象本身就已經存在的 ref,我們還可以通過 useImperativeHandle 來自定義 ref 控制器能執行哪些方法
useImperativeHandle 接收三個參數,分別是:
- ref: 組件聲明時傳入的 ref。
- createHandle: 回調函數,需要返回 ref 引用的對象,我們也是在這里重寫 ref 引用。
- deps: 依賴項數組,可選。state,props 以及內部定義的其他變量都可以作為依賴項,React 內部會使用 Object.is 來對比依賴項是否發生了變化。依賴項發生變化時,createHandle 會重新執行,ref 引用會更新。如果不傳入依賴項,那么每次更新 createHandle 都會重新執行。
在官方文檔中,有這樣一個案例,演示效果如圖所示,當我點擊按鈕時,下方的 input 自動獲取焦點,并且中間的滾動條滾動到最底部。
我們結合新的 ref 傳遞機制和 useImperativeHandle 一起來分析一下這個案例應該怎么實現。
!
思考時,請一定要把封裝的思維帶入進來,否則可能很難感受到這樣做在解耦上的具體好處
首先我們先進行組件拆分,將整個內容拆分為按鈕部分與信息部分,信息部分主要負責信息的展示與輸入,因此頁面組件大概長這樣
<>
<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,并最終再次透傳給頁面組件。因此他們的組件結構應該長這樣。
<>
<article>
<p>Welcome to my blog!</p>
</article>
<CommentList ref={commentsRef} />
<AddComment ref={addCommentRef} />
</>
這個時候,有一個需要考慮的點就是,有兩個對象需要被控制,因此我們需要借助 useImperativeHandle 來自定義控制器,并在控制的方法中,整合他們。
useImperativeHandle(ref, () => {
return {
scrollAndFocusAddComment() {
commentsRef.current.scrollToBottom();
addCommentRef.current.focus();
}
};
}, []);
ref 的傳遞,只需要把它看成是一個普通的 props 即可,因此,Post 組件完整代碼如下:
const Post = ({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} />
</>
);
}
同樣的道理,我們只需要把 CommentList 與 AddComment 的 ref 傳遞出來就可以了。
所以信息展示部分 CommentList 組件的代碼為。
import { useRef, useImperativeHandle } from 'react';
const CommentList = ({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 的代碼為。
function AddComment(props) {
return (
<input
placeholder="Add comment..."
ref={props.ref}
/>
)
};
export default AddComment;
與之前相比,確實要簡單了許多。
五、總結
如果你對封裝解耦比較重視,那么你一定能明顯感受到,ref 與 useImperativeHandle 的結合能發揮的想象空間遠不止于此,這種方式給 React 提供了一種擴展 React 能力的重要手段,因此,當你成為 React 高手之后,你一定會非常喜歡使用它們,他們的組合能讓 React 項目變得更加多樣化。