React & Redux in TypeScript - 靜態類型指南

翻譯自 react-redux-typescript-guide,做者:Piotrek Witekcss

翻譯自 TypeScriptでRedux Thunkを使う,做責:Yusuke Morihtml

參考文章 TypeScript 2.8下的終極React組件模式react

概述:最近在學習 react&TypeScript,發現有許多的組件模式和方法須要去梳理和總結。因此選擇一些文章用於沉澱和思考,記錄下一些知識點,和你們探討。git

publish:2019-03-21github

目錄:typescript

  • 簡介,環境配置(create-react-app
  • React - 關鍵類型(react-redux-typescript-guide )
  • React - 組件模式(react-redux-typescript-guide & TypeScript 2.8下的終極React組件模式)
  • Redux - 使用以及 Redux Thunk 使用(TypeScriptでRedux Thunkを使う)
  • 總結

React - 關鍵類型

  • 展現性組件(FunctionComponentredux

    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} ... />
    複製代碼

React 組件模式

  • 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 的默認屬性,主要有兩種方法:

    • 一是定義高階組件,例如TypeScript 2.8下的終極React組件模式中,利用 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 - 使用以及 Redux Thunk 使用

以以下形式來介紹Redux,主要是in-ts的使用:

  • (prestate, action) => state;
  • 使用Redux Thunk 來出來異步操做。
// 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)
複製代碼
相關文章
相關標籤/搜索