翻譯自 react-redux-typescript-guide,做者:Piotrek Witekcss
翻譯自 TypeScriptでRedux Thunkを使う,做責:Yusuke Morihtml
參考文章 TypeScript 2.8下的終極React組件模式react
概述:最近在學習 react&TypeScript,發現有許多的組件模式和方法須要去梳理和總結。因此選擇一些文章用於沉澱和思考,記錄下一些知識點,和你們探討。git
publish:2019-03-21github
目錄:typescript
展現性組件(FunctionComponent
)redux
React.FunctionComponent<P>
or React.FC<P>
。數組
const MyComponent: React.FC<Props> = ...
app
有狀態組件(ClassComponent
)異步
React.Component<P, S>
class MyComponent extends React.Component<Props, State> { ...
組件Props
React.ComponentProps<typeof Component>
獲取組件(適用於ClassComponent、FunctionComponent)的Props的類型
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
React.FC | React.Component的聯合類型
React.ComponentType<P>
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...
複製代碼
React 要素
React.ReactElement<P>
or JSX.Element
表示React元素概念的類型 - DOM組件(例如
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
React Node
React.ReactNode
表示任何類型的React節點(基本上是ReactElement(包括Fragments和Portals)+ 原始JS類型 的合集)
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />; const Component = ({ children: React.ReactNode }) => ... 複製代碼
React CSS屬性
React.CSSProperties
表明着Style Object在 JSX 文件中(一般用於 css-in-js)
const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...
複製代碼
通用的 React Event Handler
React.ReactEventHandler<HTMLElement>
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }
<input onChange={handleChange} ... />
複製代碼
特殊的 React Event Handler
React.MouseEvent<E>
| React.KeyboardEvent<E>
| React.TouchEvent<E>
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} ... />
複製代碼
Function Components - FC 純函數組件(無狀態)
顧名思義,純函數組件自己不具有 State
,因此沒有狀態,一切經過 Props
import React, { FC, ReactElement, MouseEvent } from 'react'
type Props = {
label: string,
children: ReactElement,
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
}
const FunctionComponent: FC<Props> = ({ label, children, onClick }: Props) => {
return (
<div>
<span>
{label}:
</span>
<button type="button" onClick={onClick}>
{children}
</button>
</div>
)
}
export default FunctionComponent
複製代碼
擴展屬性(spread attributes)
利用 ...
對剩餘屬性進行處理
import React, { FC, ReactElement, MouseEvent, CSSProperties } from 'react'
type Props = {
label: string,
children: ReactElement,
className?: string,
style?: CSSProperties,
onClick?: (e: MouseEvent<HTMLButtonElement>) => void,
}
const FunctionComponent: FC<Props> = ({ label, children, onClick, ...resetProps }: Props) => {
return (
<div {...resetProps}>
<span>{label}:</span>
<button type="button" onClick={onClick}>
{children}
</button>
</div>
)
}
export default FunctionComponent
複製代碼
默認屬性
若是,須要默認屬性,能夠經過默認參數值來處理
import React, { FC, ReactElement, MouseEvent } from 'react'
type Props = {
label?: string,
children: ReactElement,
}
const FunctionComponent: FC<Props> = ({ label = 'Hello', children }: Props) => {
return (
<div>
<span>
{label}:
</span>
<button type="button">
{children}
</button>
</div>
)
}
export default FunctionComponent
複製代碼
Class Components
相對於FC,多了 state
,採用以下形式來定義Class Component
這一部分的寫法,與TypeScript 2.8下的終極React組件模式相同,以爲結構很清晰,複用。
import React, { Component } from 'react';
type Props = {
label: string
}
const initialState = {
count: 0
}
type State = Readonly<typeof initialState>
class ClassCounter extends Component<Props, State> {
readonly state: State = initialState
private handleIncrement = () => this.setState(Increment)
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
<span>
{label}: {count}
</span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
)
}
}
export const Increment = (preState: State) => ({ count: preState.count + 1 })
export default ClassCounter
複製代碼
默認屬性
處理 Class Component 的默認屬性,主要有兩種方法:
withDefaultProps
來定義默認屬性,涉及組件的屬性的類型轉換;static props
以及 componentWillReceiveProps
,處理默認屬性。具體業務中,視狀況而定,第一中能夠查看相關文章,這裏介紹第二種
import React, { Component } from 'react';
type Props = {
label: string,
initialCount: number
}
type State = {
count: number;
}
class ClassCounter extends Component<Props, State> {
static defaultProps = {
initialCount: 1,
}
// 依據 defaultProps 對 state 進行處理
readonly state: State = {
count: this.props.initialCount,
}
private handleIncrement = () => this.setState(Increment)
// 響應 defaultProps 的變化
componentWillReceiveProps({ initialCount }: Props) {
if (initialCount != null && initialCount !== this.props.initialCount) {
this.setState({ count: initialCount })
}
}
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
<span>
{label}: {count}
</span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
)
}
}
export const Increment = (preState: State) => ({ count: preState.count + 1 })
export default ClassCounter
複製代碼
通用組件 Generic Components
import React, { Component, ReactElement } from 'react'
interface GenericListProps<T> {
items: T[],
itemRenderer: (item: T, i: number) => ReactElement,
}
class GenericList<T> extends Component<GenericListProps<T>, {}> {
render() {
const { items, itemRenderer } = this.props
return <div>{items.map(itemRenderer)}</div>
}
}
export default GenericList
複製代碼
Render Callback & Render Props
Render Callback,也被稱爲函數子組件,就是將 children 替換爲 () => children;
Render Props,就是將 () => component 做爲 Props 傳遞下去。
import React, { Component, ReactElement } from 'react';
type Props = {
PropRender?: () => ReactElement,
children?: () => ReactElement
}
class PropRender extends Component<Props, {}> {
render() {
const { props: { children, PropRender } }: { props: Props } = this;
return (
<div>
{ PropRender && PropRender() }
{ children && children() }
</div>
)
}
}
export default PropRender
// 應用
<PropsRender
PropRender={() => (<p>Prop Render</p>)}
>
{ () => (<p>Child Render</p>) }
</PropsRender>
複製代碼
HOC(Higher-Order Components)
簡單理解爲,接受React組件做爲輸入,輸出一個新的React組件的組件的工廠函數。
import * as React from 'react'
interface InjectedProps {
label: string
}
export const withState = <BaseProps extends InjectedProps>(
BaseComponent: React.ComponentType<BaseProps>
) => {
type HocProps = BaseProps & InjectedProps & {
initialCount?: number
}
type HocState = {
readonly count: number
}
return class Hoc extends React.Component<HocProps, HocState> {
// 方便 debugging in React-Dev-Tools
static displayName = `withState(${BaseComponent.name})`;
// 關聯原始的 wrapped component
static readonly WrappedComponent = BaseComponent;
readonly state: HocState = {
count: Number(this.props.initialCount) || 0,
}
handleIncrement = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
const { ...restProps } = this.props as any
const { count } = this.state
return (
<>
{count}
<BaseComponent
onClick={this.handleIncrement}
{...restProps}
/>
</>
)
}
}
}
複製代碼
以以下形式來介紹Redux,主要是in-ts的使用:
// store.js
type DataType = {
counter: number
}
const DataState: DataType = {
counter: 0
}
type RootState = {
Data: DataType
}
export default RootState
複製代碼
// action.js
import { Action, AnyAction } from 'redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import RootState from '../store/index'
type IncrementPayload = {
value: number
}
interface IncrementAction extends Action {
type: 'INCREMENT',
payload: IncrementPayload
}
export const Increment = ({ value }: IncrementPayload): IncrementAction => {
const payload = { value }
return {
type: 'INCREMENT',
payload
}
}
export type DecrementPayload = {
value: number;
};
export interface DecrementAction extends Action {
type: 'DECREMENT';
payload: DecrementPayload;
}
export type RootAction = IncrementAction & DecrementAction;
export const asyncIncrement = (
payload: IncrementPayload
): ThunkAction<Promise<void>, RootState, void, AnyAction> => {
return async (dispatch: ThunkDispatch<RootState, void, AnyAction>): Promise<void> => {
return new Promise<void>((resolve) => {
console.log('Login in progress')
setTimeout(() => {
dispatch(Increment(payload))
setTimeout(() => {
resolve()
}, 1000)
}, 3000)
})
}
}
複製代碼
// reducer.js
import { DataState, DataType } from '../store/Data'
import { RootAction } from '../actions/'
export default function (state: DataType = DataState, { type, payload }: RootAction): DataType {
switch(type) {
case 'INCREMENT':
return {
...state,
counter: state.counter + payload.value,
};
default:
return state;
}
}
複製代碼
// Hearder.js
import React, { Component, ReactNode } from 'react'
import RootState from '../store/index'
import { Dispatch, AnyAction } from 'redux'
import { ThunkDispatch } from 'redux-thunk'
import { connect } from 'react-redux'
import { Increment, asyncIncrement } from '../actions/'
const initialState = {
name: 'string'
}
type StateToPropsType = Readonly<{
counter: number
}>
type DispatchToPropsType = Readonly<{
handleAdd: () => void,
handleDec: () => void
}>
type StateType = Readonly<typeof initialState>
type PropsType = {
children?: ReactNode
}
type ComponentProps = StateToPropsType & DispatchToPropsType & PropsType
class Header extends Component<ComponentProps, StateType> {
readonly state: StateType = initialState;
render() {
const { props: { handleAdd, handleDec, counter }, state: { name } } = this
return (
<div>
計數:{counter}
<button onClick={handleAdd}>+</button>
<button onClick={handleDec}>-</button>
</div>
)
}
private handleClick = () => this.setState(sayHello);
}
const sayHello = (prevState: StateType) => ({
name: prevState.name + 'Hello world',
})
const mapStateToProps = (state: RootState, props: PropsType): StateToPropsType => {
return {
counter: state.Data.counter
}
}
const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, void, AnyAction>): DispatchToPropsType => {
return {
handleAdd: () => {
dispatch(Increment({ value: 2 }))
},
handleDec: async () => {
dispatch(asyncIncrement({ value: 10 }))
}
}
}
export default connect<StateToPropsType, DispatchToPropsType, PropsType, RootState>(mapStateToProps, mapDispatchToProps)(Header)
複製代碼