本文是《使用typescript写react app》系列的第一篇, 主要介绍ts在react中的基本使用.
组件是React的心脏, 下面让我们使用typescript来"修饰"React组件, 使React组件的属性具有类型.
Functional Components
函数组件在React中是一个纯函数, 一般用于展示层, 下面来看一个简单的卡片组件.
export const Card = ({ title, paragraph }) => (<aside>
<h2>{ title }</h2>
<p>
{ paragraph }
</p>
</aside>)
const el = <Card title="Welcome!" paragraph="To this example" />
首先我们需要为Card组件的参数{ title, paragraph }定义一个类型, 然后使用该类型来"修饰"Card的属性.
type CardProps = {
title: string,
paragraph: string
}
export const Card = ({ title, paragraph }: CardProps) => <aside>
<h2>{ title }</h2>
...
</aside>
虽然我们消耗了额外的开发时间定义了Card组件参数的类型, 但是我们得到了以下2个好处:
-
当我们在使用Card组件时, 编辑器会自动提示Card组件的参数类型, 以防止我们传错
-
即使传错, 在编译阶段也会提示我们传入参数类型错误 这两点好处对于我们开发中是很有帮助的, 编译阶段的错误让我们更早更快的来消除代码中的隐患, 并且在多人协作时, 使用组件时候提示参数类型可以让我们更方便的知道组件如何使用.
如果我们的Card组件中, paragraph 属性是可选的, 我们则可以使用 ? 来定义该类型
type CardProps = {
title: string,
paragraph?: string // the paragraph is optional
}
除了定义React组件的属性之外, 我们还需要定义组件的类型, 幸运的是React官方提供了类型定义, 我们可以拿来直接用, 比如我们想定义Card组件是函数组件, 我们可以这样
import React, { FunctionComponent } from 'react'; /
export const Card: FunctionComponent<CardProps> = ({ title, paragraph }) => <aside>
<h2>{ title }</h2>
<p>
{ paragraph }
</p>
</aside>
这样不仅定义了Card组件的类型是函数组件 FunctionComponent 并且也定义了Card属性的类型, 这样定义类型的方式简洁并且也是 官方推荐 的方式.
其中有一点要注意的是, 如果Card额外接收一个 children 属性的话, 我们无需为 children 定义属性, 该属性的类型会被自动分析出来.
export const Card: FunctionComponent<CardProps> = ({ title, paragraph, children }) => <aside>
<h2>{ title }</h2>
<p>
{ paragraph }
</p>
{ children }
</aside>
Class Components
类组件是老派React App的写法, 现在可以被Hooks完全替代, 我们看看如何使用Typescript修饰Class Components.
import React, { Component } from 'react';
type ClockState = {
time: Date
}
export class Clock extends Component<{}, ClockState> {
tick() {
this.setState({
time: new Date()
});
}
componentWillMount() {
this.tick();
}
componentDidMount() {
setInterval(() => this.tick(), 1000);
}
render() {
return <p>The current time is {this.state.time.toLocaleTimeString()}</p>
}
}
第十行代码中 <{}, ClockState> 的第一个空对象是来定义props类型的, 但是Clock没有props所以是空对象, 第二个参数是定义state的类型, Clock组件有一个 time 属性并且类型为javascript的 Date 类型.
Events
React有自己的事件系统, 也为每个事件定义了对应的typings, 因此我们在给事件定义类型的时候可以直接使用react typings.
import React, { Component, MouseEvent } from 'react'; // 引入typings MouseEvent
export class Button extends Component {
handleClick(event: MouseEvent) { // 定义事件类型
event.preventDefault();
alert(event.currentTarget.tagName);
}
render() {
return <button onClick={this.handleClick}>
{this.props.children}
</button>
}
}
定义了 event 是 MouseEvent 之后我们还需要为 MouseEvent也定义类型.
export class Button extends Component {
handleClick(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault();
}
// 定义MouseEvent是HTMLButtonElement或者HTMLAnchorElement类型
handleAnotherClick(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) {
event.preventDefault();
}
render() {
return <button onClick={this.handleClick}>
{this.props.children}
</button>
}
}
有一点需要注意的是, InputEvent在TS中不被支持(因为兼容性问题), 因此我们可以简单的为其定义为React合成事件
import React, { Component, SyntheticEvent } from 'react';
export class Input extends Component {
handleInput(event: SyntheticEvent) {
event.preventDefault();
// ...
}
render() {
return <>
<input type="text" onInput={this.handleInput}/>
</>
}
}
Prop Types
React提供了prop-types 来检测props的类型, 但是它是runtime时候才会去检查, 如果配合TS就完美了, 实现了 动静结合 检查props的类型, 让我们的程序更加健壮.
首先安装我们用来检查props的库:
npm install --save prop-types
npm install --save-dev @types/prop-types
我们使用 prop-types 来检查一个组件的props之后还需要再使用TS定义一遍组件的属性吗?显然是不需要的, 我们可以使用 prop-types 中的 InferProps 来帮我们做这些重复的事情.
import PropTypes, { InferProps } from "prop-types";
export function Article({
title,
price
}: InferProps<typeof Article.propTypes>) { // 使用InferProps
return (
<div className="article">
<h1>{title}</h1>
<span>Priced at (incl VAT): {price * 1.2}</span>
</div>
);
}
Article.propTypes = {
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired
};
Hooks
Hooks已经逐渐的成为了写React应用的主流方式, 如何将TS将Hooks结合起来呢? 先从 useState 这个使用最频繁之一的Hooks开始
import React, { FunctionComponent, useState } from 'react';
// 定义Counter的类型为FunctionComponent并且为useState定义属性类型
const Counter:FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
const [clicks, setClicks] = useState(initial);
return <>
<p>Clicks: {clicks}</p>
<button onClick={() => setClicks(clicks+1)}>+</button>
<button onClick={() => setClicks(clicks-1)}>-</button>
</>
}
useEffect用来处理组件的所有副作用, 比如事件监听和网络请求等. 对于useEffect我们无需定义类型, TS会去检查函数签名从而确定我们传的参数是否正确, 比如我们监听了一个 resize 事件却没有正确的提供给useEffect监听移除函数, TS就会在编译时报错.
useEffect(() => {
const handler = () => {
document.title = window.width;
}
window.addEventListener('resize', handler);
// ⚡️ won't compile
return true;
// ✅ compiles
return () => {
window.removeEventListener('resize', handler);
}
})
useRef可以帮助我们在函数组件中拿到DOM的引用, 但在 strict mode 下, TS会产生下面这种问题
function TextInputWithFocusButton() {
// 通常使用null初始化, 在render时候inputEl才会指向DOM
const inputEl = useRef(null);
const onButtonClick = () => {
// TS会报错 因为inputEl初始化时候是null
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
因此我们该避免这种类型错误, 通过判断 inputEl 是否为null
function TextInputWithFocusButton() {
// 定义useRef是一个input元素
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// 对inputEl进行null检查
if(inputEl && inputEl.current) {
inputEl.current.focus();
}
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Context API
context API是React跨父子通信的神器, 但是配合TS使用时我们会遇到一些问题
export const AppContext = React.createContext({
authenticated: true,
lang: 'en',
theme: 'dark'
});
当创建了一个context后, 我们使用 AppContext.Provider 向下传属性时必须将初始化的属性全部传递下去, 即使有些属性不是我们需要的, 否则TS就会报错
const App = () => {
// TS会报错 因为少传了其他属性
return <AppContext.Provider value={ {
lang: 'de',
} }>
<Header/>
</AppContext.Provider>
}
我们可以使用TS提供的一个工具 Partial 来解决这个问题.
type ContextProps = {
authenticated: boolean,
lang: string,
theme: string
};
export const AppContext =
React.createContext<Partial<ContextProps>>({}); // 使用Partial
const App = () => {
// TS不会再报错了
return <AppContext.Provider value={ {
authenticated: true,
} }>
<Header/>
</AppContext.Provider>
}
Conclusion
Typescript已经在逐渐的替代JavaScript, 许多优秀的开源库都迁移到了TS, 使用TS写react应用已经成为了我们必须要掌握的技能, 越早使用越早受益. 本文介绍了TS在react中最基本的使用, 在接下来的文章里面会从优秀的开源库入手, 去探索TS在react应用中的最佳实践, 敬请期待吧!
既然都看到这里了, 点个赞吧 💗