React & Redux in TypeScript - 静态类型指南

翻译自 react-redux-typescript-guide,作者:Piotrek Witek

翻译自 TypeScriptでRedux Thunkを使う,作责:Yusuke Mori

参考文章 TypeScript 2.8下的终极React组件模式

概述:最近在学习 react&TypeScript,发现有许多的组件模式和方法需要去梳理和总结。所以选择一些文章用于沉淀和思考,记录下一些知识点,和大家探讨。

publish:2019-03-21

目录:

  • 简介,环境配置(create-react-app
  • React - 关键类型(react-redux-typescript-guide )
  • React - 组件模式(react-redux-typescript-guide & TypeScript 2.8下的终极React组件模式)
  • Redux - 使用以及 Redux Thunk 使用(TypeScriptでRedux Thunkを使う)
  • 总结

React - 关键类型

  • 展示性组件(FunctionComponent

    React.FunctionComponent<P> or React.FC<P>

    const MyComponent: React.FC<Props> = ...

  • 有状态组件(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)
复制代码

猜你喜欢

转载自juejin.im/post/5c984e6651882510d82b7986