React 中需要操作元素時,可通過 findDOMNode()
或通過 createRef()
創建對元素的引用來實現。前者官方不推薦,所以這里討論后者及其與 TypeScript 結合時如何工作。
React 中的元素引用
正常的組件中,可通過創建對元素的引用來獲取到某元素然后進行相應操作。比如元素加載后將焦點定位到輸入框。
class App extends Component {
constructor(props){
super(props);
this.inputRef = React.createRef();
}
componentDidMount(){
this.inputRef.current.focus()
}
render() {
return (
<div className="App">
<input type="text" ref={this.inputRef}/>
</div>
);
}
}
創建對元素的引用是通過 React.createRef()
方法完成的。使用的時候,通過其返回對象身上的 current
屬性可訪問到綁定引用的元素。
React 內部對引用的 current
賦值更新發生在 componentDidMount
或 componentDidUpdate
生命周期之前,即存在使用的時候引用未初始化完成的情況,所以 current
不一定有值。好的做法是使用前先判空。
if(this.inputRef.current){
this.inputRef.current.focus()
}
在上面的示例中,之所以不用判空是因為我們在 componentDidMount
生命周期中使用,此時元素已經加載到頁面,所以可以放心使用。
組件中引用的傳遞
對於原生 DOM 元素可以像上面那樣創建引用,但對於自己寫的組件,則需要使用 forwardRef()
來實現。
假如你寫了個按鈕組件,想要實現像上面那樣,讓使用者可通過傳遞一個 ref
屬性來獲取到組件中原生的這個 <button>
元素以進行相應的操作。
button.jsx
const FancyInput = props => <input type="text" className="fancy-input" />;
添加 ref 支持后的按鈕組件:
button.jsx
const FancyInput = React.forwardRef((props, ref) => {
return <input type="text" ref={ref} className="fancy-input" />;
});
forwardRef
接收一個函數,函數的入參中第一個是組件的 props,第二個便是外部傳遞進來的 ref 引用。通過將這個引用在組件中綁定到相應的原生 DOM 元素上,實現了外部直接引用到組件內部元素的目的,所以叫 forwardRef
(傳遞引用)。
使用上面創建的 FancyInput
,在組件加載后使其獲得焦點:
class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
if (this.inputRef.current) {
this.inputRef.current.focus();
}
}
render() {
return (
<div className="App">
- <input type="text" ref={this.inputRef}/>
+ <FancyInput ref={this.inputRef} />
</div>
);
}
}
TypeScript 中傳遞引用
先看正常情況下,對原生 DOM 元素的引用。還是上面的示例:
class App extends Component<{}, {}> {
private inputRef = React.createRef();
componentDidMount() {
/** 🚨 Object is possibly 'null' */
this.inputRef.current.focus();
}
render() {
return (
<div className="App">
{/ 🚨 Type '{}' is missing the following properties from type 'HTMLInputElement':... /}
<input type="text" ref={this.inputRef} />
</div>
);
}
}
像上面那樣創建並使用存在兩個問題。
一個是提示我們的引用無法賦值到 <input>
的 ref
屬性上,類型不兼容。引用需要與它真實所指代的元素類型相符,這正是 TypeScript 類型檢查為我們添加的約束。這個約束的好處是,我們在使用引用的時候,就知道這個引用真實的元素類型,TypeScript 會自動提示可用的方法和屬性,同時防止調用該元素身上沒有的屬性和方法。這里修正的方法很簡單,如果 hover 或 F12 查看 React.createRef()
的方法簽名,會發現它是個泛型方法,支持傳遞類型參數。
function createRef<T>(): RefObject<T>;
所以上面創建引用時,顯式指定它的類型。
- private inputRef = React.createRef();
+ private inputRef = React.createRef<HTMLInputElement>();
第二個問題是即使在 componentDidMount
生命周期中使用,TypeScript 仍然提示 current
的值有可能為空。上面討論過,其實此時我們知道它不可能為空的。但因為 TypeScript 無法理解 componentDidMount
,所以它不知道此時引用其實是可以安全使用的。解決辦法當然是加上判空的邏輯。
componentDidMount() {
+ if(this.inputRef.current){
this.inputRef.current.focus();
+ }
}
還可通過變量后添加 !
操作符告訴 TypeScript 該變量此時非空。
componentDidMount() {
- this.inputRef.current.focus();
+ this.inputRef.current!.focus();
}
修復后完整的代碼如下:
class App extends Component<{}, {}> {
private inputRef = React.createRef<HTMLInputElement>();
componentDidMount() {
this.inputRef.current!.focus();
}
render() {
return (
<div className="App">
<input type="text" ref={this.inputRef} />
</div>
);
}
}
React + TypeScript 組件引用的傳遞
繼續到組件的情況,當需要引用的元素在另一個組件內部時,還是通過 React.forwardRef()
。
這是該方法的簽名:
function forwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
可以看到,方法接收兩個類型參數,T
為需要引用的元素類型,我們示例中是 HTMLInputElement
,P
為組件的 props 類型。
所以添加引用傳遞后,FancyInput
組件在 TypeScript 中的版本應該長這樣:
const FancyInput = React.forwardRef<HTMLInputElement, {}>((props, ref) => {
return <input type="text" ref={ref} className="fancy-input" />;
});
使用組件:
class App extends Component<{}, {}> {
private inputRef = React.createRef<HTMLInputElement>();
componentDidMount() {
this.inputRef.current!.focus();
}
render() {
return (
<div className="App">
<FancyInput ref={this.inputRef} />
</div>
);
}
}