React常见写法
贴出一份Github上star数较多的教程,react的TS写法教程。
React内置的一些类型
react本身已经有完善的TS声明文件了,所以里面有些可以拿来就用,不需要自己定义,这里说一些常见的。
React.FC<Props>
,定义函数组件用。React.Component<Props, State>
,定义类组件用。React.ReactNode
,用来定义各种 react 节点,比如<div />
,<MyComp />
,this is a just a text
,null
,0
…React.CSSProperties
,用来定义内联style
的。React.HTMLProps<HTMLXXXELEMENT>
,获取内建 html 元素的属性。React.PropsWithChildren
,如果需要显示使用 children,使用该类型创建 props,如:React.PropsWithChildren<{a: string}>
。React.ReactEventHandler<HTMLXXXELEMENT>
,用来描述元素的事件处理函数。可以自行推导出适合的事件函数描述。React.ChangeEventHandler<HTMLXXXELEMENT>
,最常见的如 input 的 change 事件函数描述,人为的判定类型。区别于上面的,这里的 event 可用 event.target.value,上面的是 event.currentTarget.value。类似还有MouseEventHandler
,MouseEventHandler
等。
React的常见写法
组件写法
类写法
interface IProps {
name: string;
}
interface IState {
age: number;
}
// 这里接收2个类型参数,分别限定组件的props和state
class ClsComp extends React.Component<IProps, IState> {
state = {
age: 18
}
}
函数写法
// 函数组件无自己的状态,因此只需要props
const FcComp: React.FC<{ prop1: string }> = ({ prop1 }) => {
return <div>{prop1}</div>
}
默认props写法
组件的props我们经常会写一些默认值,省去一些体力活。
类组件形式写法
interface IProps extends defaultProps {
age: number;
}
// 使用typeof获取到类组件的默认参数类型。这里就是{name: string}
type defaultProps = typeof DefaultPropsComp.defaultProps
class DefaultPropsComp extends React.Component<IProps> {
static defaultProps = {
name: 'jack'
}
echoProps = () => {
// 这里也可以获得TS提示
const { age, name } = this.props
}
render() {
return <p>this is how to set default props of class component</p>
}
}
函数组件形式写法
const defaultProps = {
work: 'doctor'
}
interface IProps extends defaultPropsType {
age: number;
}
type defaultPropsType = typeof defaultProps
function DefaultPropsFC(props: IProps) {
// 这里可以获得提示
const { work, age } = props
return <p>{`The ${work} is ${age}`}</p>
}
// 注意:一定要挂载默认属性,否则调用组件的地方默认值也需要传入
DefaultPropsFC.defaultProps = defaultProps
常见的如style等写法
/* 组件的定义 */
interface IProps extends DefaultProps {
descText: string // 基本的类型
style?: React.CSSProperties // 可以在调用组件的地方,获得style的完美提示。
additionDom?: React.ReactNode // 接收任何类型的React节点
renderReactNode?: () => React.ReactNode // 接收一个以函数方式返回的React节点
onChange: React.FormEventHandler<HTMLInputElement> // 优先推荐这种写法,让TS自己推断。下面的方法定义同样可以。
// onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
buttonSize: 'small' | 'middle' | 'big' // 可选参数
}
type DefaultProps = typeof UsefulReactProp.defaultProps
interface IState {
value: string
}
export default class UsefulReactProp extends React.Component<IProps, IState> {
static defaultProps = {
initVal: '输入内容'
}
// 定义一个dom引用。使用如下定义方式
static aDom = React.createRef<HTMLDivElement>()
// 定义一个类属性,可以在需要时在赋值使用
static classProp?: string
// 定义一个类属性,在定义时即复制
static classProp2 = 'hello'
state = {
value: this.props.initVal
}
defaultOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { onChange } = this.props
this.setState({
value: e.target.value
})
this.classProp = 'hello world'
onChange(e)
}
render() {
const { onChange, style, additionDom, renderReactNode } = this.props
const { value } = this.state
// 如下代码提示只有3种上述定义过的值。
const btnSize =
this.props.buttonSize === 'big'
? '100px'
: this.props.buttonSize === 'small'
? '60px'
: '80px;'
return (
<div>
<input type="text" value={value} onChange={this.defaultOnChange} />
<p className="desc" style={style}>
这是个带样式的提醒
</p>
<div className="additional">{additionDom}</div>
<div className="by-func-rcnode">
{renderReactNode ? renderReactNode() : null}
</div>
<button style={{ width: btnSize }}>按钮尺寸:联合类型提示</button>
</div>
)
}
}
/* 组件的调用 */
// 书写可以获得提示。
<UsefulReactProp
descText="一个简单的描述"
onChange={e => {
console.log(e.currentTarget.value)
}}
style={{ fontSize: 14, fontWeight: 'bold' }}
additionDom={<p>this is additional tag</p>}
renderReactNode={() => {
return <p>这是通过函数返回的ReactNode</p>
}}
buttonSize="big"
/>
React中断言
断言,即认定,确定,以及肯定。当一个值可能是多种类型,不同类型又有不同方法时,我们调用时就会出现方法不存在另外类型上,报错,这个时候就需要断言,认定一个值是我需要的类型,按照认定的类型来使用。React中的断言用 as
语法,而不是 <string>a
语法,因为尖括号语法跟JSX语法有冲突。
type UserA = {
name: string,
age: number
}
type UserB = {
name: string,
sex: string
}
type IProps = {
user: UserA | UserB
}
class AssertComp extends React.Component<IProps, {}> {
render() {
// 这里如果不做类型断言,会提示UserA上没有sex属性
// const user = this.props.user
const userB = this.props.user as UserB
const userA = this.props.user as UserA
return <div>
<p>this is a user: {userA.age}</p>
<p>this is a user: {userB.sex}</p>
</div>
}
}
Context写法
Context是React的一个特性,可以避免在嵌套组件中一层层传递props。常用的Redux库中的 Provider
组件就是利用了Context特性。
这里使用 React.createContext
类型实现。
/*
ctx.tsx
该文件,定义了我们需要的context对象
*/
export type Theme = React.CSSProperties
type Themes = {
dark: Theme
light: Theme
}
export const themes: Themes = {
dark: {
color: 'black',
backgroundColor: 'white'
},
light: {
color: 'white',
backgroundColor: 'black'
}
}
// 这里定义Context的签名
export type ThemeContextProps = { theme: Theme; toggleTheme?: () => void }
// createContext<T>需要一个类型,把定义好的Context签名传递进去
const ThemeContext = React.createContext<ThemeContextProps>({
theme: themes.light
})
/*
themeButton.tsx
最终的消费组件(消费上面定义的context对象)
*/
import ThemeContext from './ctx'
// contextType v16.6+支持
export default class ToggleThemeButton extends React.Component<{}> {
static contextType = ThemeContext
// 后面需要通过this.context访问到上下文,所以定义一个类属性
// 这里使用!明确告诉TS,该参数一定有。
// 这里不能使用?,因为context是必选项
context!: React.ContextType<typeof ThemeContext>
render() {
// 前面context必须这么写,这里才能获取TS提示
const { theme, toggleTheme } = this.context
return (
<button style={theme} onClick={toggleTheme} {...this.props}>
button
</button>
)
}
}
/*
provider组件
context对象的提供组件,相对于消费组件来说的。
*/
import ThemeContext, { Theme, themes } from './ctx'
import ThemeButton from './themeButton'
export default class ThemeProvider extends React.Component<{}, State> {
state: State = {
theme: themes.light
}
toggleTheme = () => {
this.setState(state => ({
theme: state.theme === themes.light ? themes.dark : themes.light
}))
}
render() {
const { theme } = this.state
const { toggleTheme } = this
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<ThemeButton />
</ThemeContext.Provider>
)
}
}
高阶组件HOC
先理解高阶函数,一个接收函数P作为参数,并且返回一个新的函数N的函数F,这个F即叫做高阶函数。同理,高阶组件,一个接收组件P作为参数,并且返回一个新的组件N的组件F,这个F叫做高阶组件。常见的如React-Redux库中的 connect
方法
这里写一个例子,一个 withState 函数,接收一个没有 count 状态的组件,包装后,返回一个带有 count 状态的组件。
/* withState.tsx */
export interface InjectedProps {
count: number,
onIncrement: () => void
}
export const withState = <IProp extends InjectedProps>(WrappedComp: React.ComponentType<IProp>) => {
// 使用Omit结合keyof,实现排除掉IProp和InjectedProps交集的部分(count, onIncrement)
type HocProps = Omit<IProp, keyof InjectedProps> & {
// 这里可以做HOC的Props的扩展。
initialCount?: number
}
type HocState = { readonly count: number }
return class Hoc extends React.Component<HocProps, HocState> {
// 组件的名字,方便调试
static displayName = `withState(${WrappedComp.name})`
state: HocState = {
count: Number(this.props.initialCount) || 0
}
handleInc = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
const { ...restProps } = this.props
const { count } = this.state
return (
<WrappedComp
count={count}
onIncrement={this.handleInc}
// HOC组件,应该透传与HOC无关的Prop给被包裹的组件。这里的写法是因为TS本身的BUG,如果不断言,则类型报错。
{...(restProps as IProp)}
></WrappedComp>
)
}
}
}
/*
FcCounter.tsx
暴露一个函数组件,函数组件没有自己的状态。
*/
export const FcCounter: React.FC<Props> = props => {
const { label, count, onIncrement } = props
const handleIncrement = () => {
onIncrement()
}
return (
<div>
<span>
{label}:{count}
</span>
<button type="button" onClick={handleIncrement}>{`Increment`}</button>
</div>
)
}
/* 调用组件 */
import { FcCounter } from './FcCounter'
import { withState } from './withState'
const HocCounterWithState = withState(FcCounter)
<HocCounterWithState initialCount={10} label="HocCounterWithState" />
通用组件写法
通用组件复用性较高,如 <List>
这种类型的,只是个列表容器,具体的列表每一项自定义。主要思想就是利用泛型。
// 组件定义
interface IProps<T> {
lists: T[]
renderItem: (item: T) => React.ReactNode // 最好都用ReactNode,而不是JSX.Element。
}
class List<T> extends React.Component<IProps<T>, {}> {
render() {
const {lists, renderItem} = this.props
return (
<>
<ul>{lists.map(renderItem)}</ul>
</>
)
}
}
// 调用组件。注意:这里List传入了一个类型number,限制了泛型T的类型。
<List<number> lists={[1, 2, 3]} renderItem={item => <li key={item}>{item}</li>}/>
useRef+useImperativeHandle+forwardRef写法
函数组件没有实例,如何在父组件拿到函数子组件的方法呢?
// 父组件
import { useRef } from 'react';
import Filter from './components/Filter';
type FilterHandle = React.ElementRef<typeof Filter> // *
function Parent() {
const FilterRef = useRef<FilterHandle>(null) // *
return <Filter ref={FilterRef} /> // *
}
// 子组件
import { useImperativeHandle, forwardRef } from 'react';
import {Form} from 'antd'
// 自定义返回的函数组件方法
type FilterHandle = {
getFieldsValue: () => {[key: string]: any}
}
type FilterProps = {}
// TS定义下是一个ForwardRef组件
const Filter: React.ForwardRefRenderFunction<FilterHandle, FilterProps> = (props, ref) => {
const [form] = Form.useForm();
// 生成返回给父组件的自定义实例值
useImperativeHandle(ref, () => ({
getFieldsValue() {
// 可以暴露出form的一些方法
return form.getFieldsValue()
}
}));
return (
<Form className={styles.wrap} form={form}>
<Row gutter={12}>
<Col span={6}>
<Form.Item label="账号名称" name="name">
<Input placeholder="请输入账号名称" />
</Form.Item>
</Col>
<Col span={18} style={{ textAlign: 'right' }}>
<Form.Item>
<Space size={16}>
<Button>重置</Button>
<Button type="primary">查询</Button>
</Space>
</Form.Item>
</Col>
</Row>
</Form>
);
}
export default forwardRef(Filter);