Окончательный шаблон компонента React в TypeScript 2.8

внешний интерфейс Безопасность TypeScript React.js
Окончательный шаблон компонента React в TypeScript 2.8

Профиль переводчикаzqluТехническая группа Ant Financial Data Experience

Переведено сUltimate React Component Patterns with Typescript 2.8,авторMartin Hochel

Этот блогReact Component Patternsчтобы вдохновить

Онлайн-демонстрация

Компоненты с состоянием, Компоненты без состояния, Свойства по умолчанию, Обратные вызовы рендеринга, Внедрение компонентов, Общие компоненты, Компоненты более высокого порядка, Управляемые компоненты

Если вы меня знаете, вы уже знаете, что я не пишу код javascript без определений типов, поэтому мне очень нравится TypeScript, начиная с версии 0.9. Помимо типизированного JS, мне также очень нравится библиотека React, поэтому при объединении React и Typescript для меня это было как рай :). Полная безопасность типов во всем приложении и виртуальный DOM — это фантастика и удовольствие.

Так о чем эта статья? В Интернете есть различные статьи о шаблонах компонентов React, но ничего о том, как применить эти шаблоны к Typescript. Кроме того, в предстоящем выпуске TS 2.8 представлены захватывающие новые функции, такие как условные типы, новые предопределенные условные типы в стандартной библиотеке, гомоморфные модификаторы типов карт и т. д. Функция заключается в том, что мы можем легко создавать общие шаблоны компонентов React в безопасном для типов способ.

Эта статья будет длинной, так что откиньтесь на спинку кресла и расслабьтесь, пока вы осваиваете окончательный шаблон компонента React в Typescript.

Все режимы/примеры используют машинописную версию версии 2.8 и строгий режим.

Готов начать

Во-первых, нам нужно установить вспомогательные библиотеки typescript и tslibs, чтобы мы могли генерировать меньший код.

yarn add -D typescript@next
# tslib 将仅用与您的编译目标不支持的功能
yarn add tslib

Таким образом, мы можем инициализировать нашу конфигурацию typescript:

# 这条命令将在我们的工程中创建默认配置 tsconfig.json 
yarn tsc --init

Теперь давайте установим react, react-dom и определения их типов.

yarn add react react-dom
yarn add -D @types/{react,react-dom}

Потрясающий! Теперь мы можем перейти в наш компонентный режим, не так ли?

компоненты без сохранения состояния

Как вы уже догадались, это компоненты без состояния (также известные как презентационные компоненты). Отчасти они также являются чисто функциональными компонентами. Давайте создадим искусственный компонент Button без состояния в TypeScript.

Как и в случае с нативным JS, нам нужно импортировать React, чтобы мы могли использовать JSX.

import React from 'react'

const Button = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
)

Хотя компилятор tsc и сейчас выдает ошибку! Нам нужно явно указать нашему компоненту/функции, какого типа наши реквизиты. Давайте определим наши реквизиты:

import React, { MouseEvent, ReactNode } from 'react'
type Props = { 
 onClick(e: MouseEvent<HTMLElement>): void
 children?: ReactNode 
}

const Button = ({ onClick: handleClick, children }: Props) => (
  <button onClick={handleClick}>{children}</button>
)

Теперь мы исправили все ошибки! очень хороший! Но мы можем лучше!

существует@types/reactТип был предопределен вtype SFC<P>, который также относится к типуinterface StatelessComponent<P>псевдоним для , кроме того, он уже имеет предопределенныйchildrenи другие (defaultProps, displayName и т. д.), поэтому нам не нужно каждый раз писать это самим!

Таким образом, окончательный компонент без сохранения состояния выглядит так:

import React, { MouseEvent, SFC } from 'react';

type Props = { onClick(e: MouseEvent<HTMLElement>): void };

const Button: SFC<Props> = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
);

компоненты с состоянием

Давайте используем наш компонент Button для создания компонента счетчика с отслеживанием состояния.

Сначала нам нужно определитьinitialState

const initialState = { clicksCount: 0 }

Теперь мы будем использовать TypeScript для вывода типа состояния из нашей реализации.

Таким образом, нам не нужно поддерживать наши определения типов и реализации отдельно, у нас есть только один источник правды, наша реализация, отлично!

type State = Readonly<typeof initialState>

Также обратите внимание, что тип явно сопоставлен, чтобы сделать все свойства доступными только для чтения. Нам нужно снова использовать тип State, чтобы явно определить свойство состояния только для чтения в нашем классе.

readonly state: State = initialState

Что это делает?

Мы знаем, что не можем обновлять напрямую, как показано ниже в React.state:

this.state.clicksCount = 2;
this.state = { clicksCount: 2 }

Это приведет к ошибке во время выполнения, но не во время компиляции. явно используяReadonlyкарта нашегоtype State, и установив свойство состояния только для чтения в нашем определении класса, TS сразу сообщит нам, что мы делаем что-то не так.

Пример: состояние безопасности типов во время компиляции

22.gif | left | 827x289

Реализация всего компонента контейнера/компонента с отслеживанием состояния:

В нашем компоненте-контейнере еще нет Props API, поэтому нам нужно добавитьCompoentПервый общий параметр компонента определяется какObject(потому что в Реактеpropsвсегда возражать{}), и использоватьStateвведите в качестве второго универсального параметра.

import React, { Component } from 'react';

import Button from './Button';

const initialState = { clicksCount: 0 };
type State = Readonly<typeof initialState>;

class ButtonCounter extends Component<object, State> {
  readonly state: State = initialState;

  render() {
    const { clicksCount } = this.state;
    return (
      <>
        <Button onClick={this.handleIncrement}>Increment</Button>
        <Button onClick={this.handleDecrement}>Decrement</Button>
        You've clicked me {clicksCount} times!
      </>
    );
  }

  private handleIncrement = () => this.setState(incrementClicksCount);
  private handleDecrement = () => this.setState(decrementClicksCount);
}

const incrementClicksCount = (prevState: State) => ({
  clicksCount: prevState.clicksCount + 1,
});
const decrementClicksCount = (prevState: State) => ({
  clicksCount: prevState.clicksCount - 1,
});

Вы могли заметить, что мы выделили функцию обновления состояния вне класса как чистую функцию. Это общий шаблон, так что мы можем просто протестировать эти функции обновления состояния, не зная логики рендеринга. Кроме того, поскольку мы используем TypeScript и явно отображаем состояние как доступное только для чтения, это не позволит нам выполнять некоторые операции по изменению состояния в этих функциях:

const decrementClicksCount = (prevState: State) => ({
  clicksCount: prevState.clicksCount--,
});

// 这样讲抛出编译错误:
//
// [ts] Cannot assign to 'clicksCount' because it is a constant or a read-only property.

Очень круто, правда? :)


свойства по умолчанию

Давайте расширим наш компонент Button, чтобы добавить свойство цвета строкового типа.

type Props = {
  onClick(e: MouseEvent<HTMLElement>): void;
  color: string;
};

Если мы хотим определить свойства по умолчанию, мы можем передать наш компонентButton.defaultProps = {…}определять.

Сделав это, нам нужно изменить определение типа нашего свойства, чтобы пометить свойство как необязательное со значением по умолчанию.

Таким образом, определение выглядит так (примечание?оператор)

type Props = {
  onClick(e: MouseEvent<HTMLElement>): void;
  color?: string;
};

На данный момент реализация нашего компонента выглядит так:

const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
  <button style={{ color }} onClick={handleClick}>
    {children}
  </button>
);

Хотя это работает в нашем простом примере, есть проблема. Поскольку мы стираем в строгом режиме, необязательные атрибутыcolorТип является объединениемundefined | string.

Например, мы хотим что-то сделать со свойством цвета, TS выдаст ошибку, потому что не знает, что оно было создано React.Component.defaultPropsбыл определен в:

33.gif | left | 813x255

Чтобы удовлетворить компилятор TS, мы можем использовать следующие 3 метода:

  • Используйте оператор __!__ в функции рендеринга, чтобы явно указать компилятору, что эта переменная не будетundefined, хотя это необязательно, например:<button onClick={handleClick!}>{children}</button>
  • Используйте __условный оператор/тернарный оператор__, чтобы сообщить компилятору, что некоторые свойства не определены:<button onClick={handleClick ? handleClick : undefined}>{children}</button>
  • Создайте переносимый __withDefaultProps__ Функция более высокого порядка, которая обновит определение типа реквизита и установит свойства по умолчанию. Я думаю, что это самое краткое и чистое решение.

Мы можем легко реализовать нашу функцию высшего порядка (благодаря условным картам типов TS 2.8):

export const withDefaultProps = <
  P extends object,
  DP extends Partial<P> = Partial<P>
>(
  defaultProps: DP,
  Cmp: ComponentType<P>,
) => {
  // 提取出必须的属性
  type RequiredProps = Omit<P, keyof DP>;
  // 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
  type Props = Partial<DP> & Required<RequiredProps>;

  Cmp.defaultProps = defaultProps;

  // 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
  return (Cmp as ComponentType<any>) as ComponentType<Props>;
};

Теперь мы можем использоватьwithDefaultPropsФункции более высокого порядка для определения наших свойств по умолчанию, но также решают предыдущую проблему:

const defaultProps = {
  color: 'red',
};

type DefaultProps = typeof defaultProps;
type Props = { onClick(e: MouseEvent<HTMLElement>): void } & DefaultProps;

const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
  <button style={{ color }} onClick={handleClick}>
    {children}
  </button>
);

const ButtonWithDefaultProps = withDefaultProps(defaultProps, Button);

Или используйте inline напрямую (обратите внимание, что нам нужно явно предоставить определение свойства исходного компонента Button, TS не может вывести тип параметра из функции):

const ButtonWithDefaultProps = withDefaultProps<Props>(
  defaultProps,
  ({ onClick: handleClick, color, children }) => (
    <button style={{ color }} onClick={handleClick}>
      {children}
    </button>
  ),
);

Теперь свойства компонента Button правильно определены и используются, свойства по умолчанию отражены и необязательны в определении типа, но обязательны в реализации!

{
    onClick(e: MouseEvent<HTMLElement>): void
    color?: string
}

44.png | left | 827x83

Использование компонентов остается прежним:

render() {
    return (
        <ButtonWithDefaultProps
            onClick={this.handleIncrement}
        >
        	Increment
        </ButtonWithDefaultProps>
    )
}

Конечно, это также используется сclassОпределенные компоненты (благодаря происхождению структуры классов в TS нам не нужно явно указывать нашиPropsуниверсальный тип).

Это выглядит так:

const ButtonViaClass = withDefaultProps(
  defaultProps,
  class Button extends Component<Props> {
    render() {
      const { onClick: handleClick, color, children } = this.props;
      return (
        <button style={{ color }} onClick={handleClick}>
          {Children}
        </button>
      );
    }
  },
);

Опять же, он по-прежнему используется так же:

render() {
  return (
    <ButtonViaClass onClick={this.handleIncrement}>Increment</ButtonViaClass>
  );
}

Допустим, вам нужно создать расширяемый компонент меню, который должен отображать дочерний контент, когда пользователь нажимает на него. Для этого мы можем использовать различные шаблоны компонентов.

визуализировать режим обратного вызова/рендеринга свойств

Лучший способ добиться повторного использования логики компонента — поместить дочерние элементы компонента в функции или использоватьrenderAPI свойств. Вот почему обратные вызовы рендеринга также называются подкомпонентами функций.

Давайте реализуем метод свойства рендерингаToggleableКомпоненты:

import React, { Component, MouseEvent } from 'react';
import { isFunction } from '../utils';

const initialState = {
  show: false,
};

type State = Readonly<typeof initialState>;
                      
type Props = Partial<{
  children: RenderCallback;
  render: RenderCallback;
}>;

type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
  show: State['show'];
  toggle: Toggleable['toggle'];
};

export class Toggleable extends Component<Props, State> {
  readonly state: State = initialState;

  render() {
    const { render, children } = this.props;
    const renderProps = {
      show: this.state.show,
      toggle: this.toggle,
    };

    if (render) {
      return render(renderProps);
    }

    return isFunction(children) ? children(renderProps) : null;
  }

  private toggle = (event: MouseEvent<HTMLElement>) =>
    this.setState(updateShowState);
}

const updateShowState = (prevState: State) => ({ show: !prevState.show });

Что здесь происходит, давайте рассмотрим важные части отдельно:

const initialState = {
  show: false,
};
type State = Readonly<typeof initialState>;
  • Здесь мы объявляем наше состояние, как в предыдущем примере

Теперь давайте определим свойства компонента (обратите внимание, что здесь мы используем тип отображения Partitial, потому что все наши свойства являются необязательными, и нам не нужно вручную добавлять каждое свойство отдельно).?идентификатор):

type Props = Partial<{
  children: RenderCallback;
  render: RenderCallback;
}>;

type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
  show: State['show'];
  toggle: Toggleable['toggle'];
};

Нам нужно поддерживать как дочерний элемент как функцию, так и свойство рендеринга как функцию, оба из которых являются необязательными. Чтобы избежать дублирования кода, мы определяемRenderCallbackКак определение нашей функции рендеринга:

type RenderCallback = (args: ToggleableComponentProps) => JSX.Element

Часть, которая кажется читателю странной, — это наш окончательный тип псевдонима:type ToggleableComponentProps!

type ToggleableComponentProps = {
  show: State['show'];
  toggle: Toggleable['toggle'];
};

Здесь мы используем __lookup types__ TypeScript, поэтому нам не нужно повторно определять типы:

  • show: State['show']Мы используем существующее определение типа состоянияshowАтрибуты
  • toggle: Toggleable['toggle']Мы используем TS, чтобы вывести тип класса из реализации класса, чтобы определитьtoggleАтрибуты. Очень полезно и очень мощно.

Остальная часть реализации проста, стандартнаявизуализировать свойство/дочерние элементы как функциюРежим:

export class Toggleable extends Component<Props, State> {
  // ...
  render() {
    const { render, children } = this.props;
    const renderProps = {
      show: this.state.show,
      toggle: this.toggle,
    };

    if (render) {
      return render(renderProps);
    }

    return isFunction(children) ? children(renderProps) : null;
  }
  // ...
}

Теперь мы можем передать функцию как дочернюю компоненту Toggleable:

<Toggleable>
  {({ show, toggle }) => (
    <>
      <div onClick={toggle}>
        <h1>some title</h1>
      </div>
      {show ? <p>some content</p> : null}
    </>
  )}
</Toggleable>

Или мы можем поместить функцию как свойство рендеринга:

<Toggleable
  render={({ show, toggle }) => (
    <>
      <div onClick={toggle}>
        <h1>some title</h1>
      </div>
      {show ? <p>some content</p> : null}
    </>
  )}
/>

Благодаря TypeScript у нас есть intellisense и правильная проверка типов для параметров свойства рендеринга:

55.gif | left | 674x370

Если мы хотим использовать его повторно (например, в нескольких компонентах меню), нам просто нужно создать компонент сердца, использующий логику Toggleable:

type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
  <Toggleable
    render={({ show, toggle }) => (
      <>
        <div onClick={toggle}>
          <h1>title</h1>
        </div>
        {show ? children : null}
      </>
    )}
  />
)

Теперь наш новый __ToggleableMenuКомпонент __ уже доступен в компоненте Menu:

export class Menu extends Component {
  render() {
    return (
      <>
        <ToggleableMenu title="First Menu">Some content</ToggleableMenu>
        <ToggleableMenu title="Second Menu">Some content</ToggleableMenu>
        <ToggleableMenu title="Third Menu">Some content</ToggleableMenu>
      </>
    );
  }
}

и это также работает, как мы ожидаем:

66.gif | left | 647x479

Этот шаблон очень полезен, когда мы хотим изменить визуализируемый контент, но не заботимся об изменении состояния: как видите, мы переместили логику рендеринга в дочернюю функцию компонента ToggleableMenu, но сохранили логику управления состоянием в наш переключаемый компонент!

впрыск компонентов

Чтобы сделать наши компоненты более гибкими, мы можем ввести шаблон внедрения компонентов.

Какова схема впрыска компонентов? Если вы знакомы с React-Router, вы уже используете этот шаблон при определении маршрутов следующим образом:

<Route path="/foo" component={MyView} />

Таким образом, вместо того, чтобы передавать функции свойствам render/children, мы передаемcomponentСвойство «внедряет» компонент. Для этого мы можем реорганизовать и изменить нашу встроенную функцию свойства рендеринга на многоразовый компонент без состояния:

type MenuItemProps = { title: string };
const MenuItem: SFC<MenuItemProps & ToggleableComponentProps> = ({
  title,
  toggle,
  show,
  children,
}) => (
  <>
    <div onClick={toggle}>
      <h1>{title}</h1>
    </div>
    {show ? children : null}
  </>
);

При этом мы можем провести рефакторинг, используя свойство рендерингаToggleableMenu:

type Props = { title: string };
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
  <Toggleable
    render={({ show, toggle }) => (
      <MenuItem show={show} toggle={toggle} title={title}>
        {children}
      </MenuItem>
    )}
  />
);

Сделав это, давайте начнем определять наш новый API —compoentАтрибуты.

Нам нужно обновить наш API свойств.

  • childrenТеперь может быть функцией или ReactNode (когда используется компонентная поддержка)
  • componentнаш новый API, его можно реализоватьToggleableComponentPropsсвойств компонента, и он должен быть универсальным набором для любого, чтобы различные реализующие компоненты могли добавлять другие свойства кToggleableComponentPropsи проверено ТС
  • propsВведем определения, которые могут переходить в произвольные свойства. Он определен как индексируемый тип типа any, и здесь мы ослабляем строгие проверки безопасности типов...
// 我们需要使用我们任意的props类型来创建 defaultProps,默认是一个空对象
const defaultProps = { props: {} as { [name: string]: any } };
type Props = Partial<
  {
    children: RenderCallback | ReactNode;
    render: RenderCallback;
    component: ComponentType<ToggleableComponentProps<any>>;
  } & DefaultProps
>;
type DefaultProps = typeof defaultProps;

Далее нам нужно добавить новый API свойств вToggleableComponentProps, чтобы пользователи могли<Toggleable props={...} />использовать нашpropsАтрибуты:

export type ToggleableComponentProps<P extends object = object> = {
  show: State['show'];
  toggle: Toggleable['toggle'];
} & P;

Затем нам нужно обновить нашrenderфункция:

  render() {
    const {
      component: InjectedComponent,
      props,
      render,
      children,
    } = this.props;
    const renderProps = {
      show: this.state.show,
      toggle: this.toggle,
    };

    // 当 component 属性被使用时,children 是 ReactNode 而不是函数
    if (InjectedComponent) {
      return (
        <InjectedComponent {...props} {...renderProps}>
          {children}
        </InjectedComponent>
      );
    }

    if (render) {
      return render(renderProps);
    }

    return isFunction(children) ? children(renderProps) : null;
  }

Полный компонент Toggleable реализован следующим образом, поддерживая свойство рендеринга, дочерние элементы как функцию и внедрение компонентов:

import React, { Component, ReactNode, ComponentType, MouseEvent } from 'react';

import { isFunction, getHocComponentName } from '../utils';

const initialState = { show: false };
const defaultProps = { props: {} as { [name: string]: any } };

type State = Readonly<typeof initialState>;
type Props = Partial<
  {
    children: RenderCallback | ReactNode;
    render: RenderCallback;
    component: ComponentType<ToggleableComponentProps<any>>;
  } & DefaultProps
>;

type DefaultProps = typeof defaultProps;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;

export type ToggleableComponentProps<P extends object = object> = {
  show: State['show'];
  toggle: Toggleable['toggle'];
} & P;

export class Toggleable extends Component<Props, State> {
  static readonly defaultProps: Props = defaultProps;
  readonly state: State = initialState;

  render() {
    const {
      component: InjectedComponent,
      props,
      render,
      children,
    } = this.props;
    const renderProps = {
      show: this.state.show,
      toggle: this.toggle,
    };

    if (InjectedComponent) {
      return (
        <InjectedComponent {...props} {...renderProps}>
          {children}
        </InjectedComponent>
      );
    }

    if (render) {
      return render(renderProps);
    }

    return isFunction(children) ? children(renderProps) : null;
  }

  private toggle = (event: MouseEvent<HTMLElement>) =>
    this.setState(updateShowState);
}

const updateShowState = (prevState: State) => ({ show: !prevState.show });

мы закончили тем, что использовалиcomponentатрибутToggleableMenuViaComponentInjectionКомпоненты такие:

const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
  title,
  children,
}) => (
  <Toggleable component={MenuItem} props={{ title }}>
    {children}
  </Toggleable>
);

Пожалуйста, обрати внимание, вот нашpropsСвойство не имеет строгой проверки безопасности типов, поскольку оно определено как тип индексированного объекта.{ [name: string]: any }:

77.gif | left | 827x279

Мы все еще можем использовать то же самоеToggleableMenuViaComponentInjectionкомпонент для реализации рендеринга меню:


export class Menu extends Component {
  render() {
    return (
      <>
        <ToggleableMenuViaComponentInjection title="First Menu">
          Some content
        </ToggleableMenuViaComponentInjection>
        <ToggleableMenuViaComponentInjection title="Second Menu">
          Another content
        </ToggleableMenuViaComponentInjection>
        <ToggleableMenuViaComponentInjection title="Third Menu">
          More content
        </ToggleableMenuViaComponentInjection>
      </>
    );
  }
}

Общие компоненты

Когда мы смотрим на «шаблон впрыска компонентов», мы теряемpropsСтрогая проверка безопасности типов свойств. Как мы можем решить эту проблему? Да, вы угадали! мы можем поставить нашToggleableКомпонент реализован как общий компонент!

Сначала нам нужно сделать наши свойства универсальными. Мы используем общий параметр по умолчанию, поэтому нам не нужно явно указывать тип, когда это не нужно (для свойства рендеринга и дочерних элементов как функций).

type Props<P extends object = object> = Partial<
  {
    children: RenderCallback | ReactNode;
    render: RenderCallback;
    component: ComponentType<ToggleableComponentProps<P>>;
  } & DefaultProps<P>
>;

Нам также необходимоToggleableComponnetPropsОбновление до универсального. Нет, подождите, это уже родовое! Так что никаких изменений пока не требуется.

Что нужно обновить, так этоtype DefaultProps, поскольку вывод определений универсального типа из объявленных реализаций не поддерживается, его необходимо реорганизовать в традиционные определения типов -> реализации:

type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };

Это почти там!

Теперь давайте также сделаем класс компонента универсальным. Опять же, мы используем свойства по умолчанию, поэтому нет необходимости указывать общие параметры, когда не используется внедрение компонентов!

export class Toggleable<T = {}> extends Component<Props<T>, State> {}

Это сделано? Хм... можем ли мы использовать универсальные типы в JSX?

Плохая новость в том, что нельзя...

Но мы можем ввести общие компонентыofTypeРежим мастерской:

export class Toggleable<T = {}> extends Component<Props<T>, State> {
  static ofType<T extends object>() {
    return Toggleable as Constructor<Toggleable<T>>;
  }
}

Полная реализация компонента Toggleable с поддержкой свойства Render, Children как функции и внедрения компонента с поддержкой общих свойств props:

import React, {
  Component,
  ReactNode,
  ComponentType,
  MouseEvent,
  SFC,
} from 'react';

import { isFunction, getHocComponentName } from '../utils';

const initialState = { show: false };
// const defaultProps = { props: {} as { [name: string]: any } };

type State = Readonly<typeof initialState>;
type Props<P extends object = object> = Partial<
  {
    children: RenderCallback | ReactNode;
    render: RenderCallback;
    component: ComponentType<ToggleableComponentProps<P>>;
  } & DefaultProps<P>
>;

type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;

export type ToggleableComponentProps<P extends object = object> = {
  show: State['show'];
  toggle: Toggleable['toggle'];
} & P;

export class Toggleable<T = {}> extends Component<Props<T>, State> {
  static ofType<T extends object>() {
    return Toggleable as Constructor<Toggleable<T>>;
  }
  static readonly defaultProps: Props = defaultProps;
  readonly state: State = initialState;

  render() {
    const {
      component: InjectedComponent,
      props,
      render,
      children,
    } = this.props;
    const renderProps = {
      show: this.state.show,
      toggle: this.toggle,
    };

    if (InjectedComponent) {
      return (
        <InjectedComponent {...props} {...renderProps}>
          {children}
        </InjectedComponent>
      );
    }

    if (render) {
      return render(renderProps);
    }

    return isFunction(children) ? children(renderProps) : null;
  }

  private toggle = (event: MouseEvent<HTMLElement>) =>
    this.setState(updateShowState);
}

const updateShowState = (prevState: State) => ({ show: !prevState.show });

имеютstatic ofTypeПосле фабричной функции мы можем создать универсальный компонент нужного типа.

type MenuItemProps = { title: string };
// ofType 是一种标识函数,返回的是相同实现的 Toggleable 组件,但带有制定的 props 类型
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();

type ToggleableMenuProps = MenuItemProps;
const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
  title,
  children,
}) => (
  <ToggleableWithTitle component={MenuItem} props={{ title }}>
    {children}
  </ToggleableWithTitle>
);

и все до сих пор работает как надо, но на этот раз у меняprops={}Свойства имеют правильную проверку типов. Подарите ему аплодисменты!

Type Safe | left

компоненты более высокого порядка

Поскольку мы создали функцию обратного вызова рендерингаToggleableКомпоненты, реализовать HOC тоже будет легко. (Это также большое преимущество шаблона функции обратного вызова рендеринга, потому что мы можем использовать HOC для его достижения)

Приступим к реализации нашего компонента HOC:

Нам нужно создать:

  • displayName (чтобы мы могли хорошо отлаживать в devtools)
  • WrappedComponent (чтобы мы могли получить исходный компонент — полезно для тестирования)
  • использоватьhoist-non-react-staticsв нпм пакетеhoistNonReactStatics
import React, { ComponentType, Component } from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';

import { getHocComponentName } from '../utils';

import {
  Toggleable,
  Props as ToggleableProps,
  ToggleableComponentProps,
} from './RenderProps';

// OwnProps 是内部组件上任意公开的属性
type OwnProps = object;
type InjectedProps = ToggleableComponentProps;

export const withToggleable = <OriginalProps extends object>(
  UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>,
) => {
  // 我们使用 TS 2.8 中的条件映射类型来得到我们最终的属性类型
  type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps;

  class WithToggleable extends Component<Props> {
    static readonly displayName = getHocComponentName(
      WithToggleable.displayName,
      UnwrappedComponent,
    );
    static readonly UnwrappedComponent = UnwrappedComponent;

    render() {
      const { ...rest } = this.props;

      return (
        <Toggleable
          render={renderProps => (
            <UnwrappedComponent {...rest} {...renderProps} />
          )}
        />
      );
    }
  }

  return hoistNonReactStatics(WithToggleable, UnwrappedComponent);
};

Теперь мы можем использовать HOC для создания нашегоToggleableКомпонент меню больше не используется и имеет правильные проверки безопасности типов!

const ToggleableMenuViaHOC = withToggleable(MenuItem)

Все работает нормально, плюс проверка безопасности типа! отличный!

99.gif | left | 812x293

Управляемые компоненты

Это последний шаблон компонента! Предположим, мы хотим контролировать нашуToggleableкомпоненты нам нужныToggleableКонфигурация компонента. Это очень мощный режим. Давайте сделаем это.

Когда я говорю контролируемый компонент, что я имею в виду? я хочу отMenuТаким образом, элемент управления внутри компонентаToggleableManuОтображается ли содержимое компонента.

100.gif | left | 656x512

Нам нужно обновить наш, как этоToggleableMenuРеализация компонента:

// 更新我们的属性类型,以便我们可以通过 show 属性来控制是否显示
type Props = MenuItemProps & { show?: boolean };

// 注意:这里我们使用了结构来创建变量别,为了不和 render 回调函数的 show 参数冲突
// -> { show: showContent }

// Render 属性
export const ToggleMenu: SFC<ToggleableComponentProps> = ({
  title,
  children,
  show: showContent,
}) => (
  <Toggleable show={showContent}>
    {({ show, toggle }) => (
      <MenuItem title={title} toggle={toggle} show={show}>
        {children}
      </MenuItem>
    )}
  </Toggleable>
);

// 组件注入
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();

export const ToggleableMenuViaComponentInjection: SFC<Props> = ({
  title,
  children,
  show: showContent,
}) => (
  <ToggleableWithTitle
    component={MenuItem}
    props={{ title }}
    show={showContent}
  >
    {children}
  </ToggleableWithTitle>
);

// HOC不需要更改
export const ToggleMenuViaHOC = withToggleable(MenuItem);

Благодаря этим обновлениям мы можемMenuдобавить состояние и перейти кToggleableMenu

const initialState = { showContents: false };
type State = Readonly<typeof initialState>;

export class Menu extends Component<object, State> {
  readonly state: State = initialState;
  render() {
    const { showContents } = this.state;
    return (
      <>
        <button onClick={this.toggleShowContents}>toggle showContent</button>
        <hr />
        <ToggleableMenu title="First Menu" show={showContents}>
          Some Content
        </ToggleableMenu>
        <ToggleableMenu title="Second Menu" show={showContents}>
          Another Content
        </ToggleableMenu>
        <ToggleableMenu title="Third Menu" show={showContents}>
          More Content
        </ToggleableMenu>
      </>
    );
  }
}

Сделаем последнее обновление для максимальной функциональности и гибкости.Toggleableкомпоненты. Чтобы сделать Toggleable управляемым компонентом, нам нужно:

  1. Добавить кshowприписыватьPropsна API
  2. Обновите свойства по умолчанию (поскольку показать необязательно)
  3. Обновите начальное состояние компонента из Props.show, ведь теперь значение в нашем состоянии может зависеть от свойств, переданных из родительского компонента
  4. Обновление состояния из реквизита в функции жизненного цикла componentWillReceiveProps

1 & 2:

const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>

3 & 4:

export class Toggleable<T = {}> extends Component<Props<T>, State> {
  static readonly defaultProps: Props = defaultProps
  // Bang operator used, I know I know ...
  state: State = { show: this.props.show! }
  componentWillReceiveProps(nextProps: Props<T>) {
    const currentProps = this.props
    if (nextProps.show !== currentProps.show) {
      this.setState({ show: Boolean(nextProps.show) })
    }
  }
}

Наконец, компонент Toggleable, который поддерживает все режимы (свойство рендеринга/дочерние элементы как функция/внедрение компонента/общий компонент/управляемый компонент):

import React, { Component, MouseEvent, ComponentType, ReactNode } from 'react'

import { isFunction, getHocComponentName } from '../utils'

const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
export type Props<P extends object = object> = Partial<
  {
    children: RenderCallback | ReactNode
    render: RenderCallback
    component: ComponentType<ToggleableComponentProps<P>>
  } & DefaultProps<P>
>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
export type ToggleableComponentProps<P extends object = object> = {
  show: State['show']
  toggle: Toggleable['toggle']
} & P
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>

export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
  static ofType<T extends object>() {
    return Toggleable as Constructor<Toggleable<T>>
  }
  static readonly defaultProps: Props = defaultProps
  readonly state: State = { show: this.props.show! }

  componentWillReceiveProps(nextProps: Props<T>, nextContext: any) {
    const currentProps = this.props

    if (nextProps.show !== currentProps.show) {
      this.setState({ show: Boolean(nextProps.show) })
    }
  }
  render() {
    const { component: InjectedComponent, children, render, props } = this.props
    const renderProps = { show: this.state.show, toggle: this.toggle }

    if (InjectedComponent) {
      return (
        <InjectedComponent {...props} {...renderProps}>
          {children}
        </InjectedComponent>
      )
    }

    if (render) {
      return render(renderProps)
    }

    return isFunction(children) ? children(renderProps) : new Error('asdsa()')
  }
  private toggle = (event: MouseEvent<HTMLElement>) => this.setState(updateShowState)
}

const updateShowState = (prevState: State) => ({ show: !prevState.show })

Окончательный компонент Toggleable HOC withToggleable

Просто небольшая модификация -> нам нужно передать компонент HOCshowсвойств и обновить нашиOwnPropsAPI

import React, { ComponentType, Component } from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'

import { getHocComponentName } from '../utils'

import {
  Toggleable,
  Props as ToggleableProps,
  ToggleableComponentProps as InjectedProps,
} from './toggleable'

// OwnProps is for any public props that should be available on internal Component.props
// and for WrappedComponent
type OwnProps = Pick<ToggleableProps, 'show'>

export const withToogleable = <OriginalProps extends object>(
  UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>
) => {
  // we are leveraging TS 2.8 conditional mapped types to get proper final prop types
  type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps
  class WithToggleable extends Component<Props> {
    static readonly displayName = getHocComponentName(
      WithToggleable.displayName,
      UnwrappedComponent
    )
    static readonly WrappedComponent = UnwrappedComponent
    render() {
      // Generics and spread issue
      // https://github.com/Microsoft/TypeScript/issues/10727
      const { show, ...rest } = this.props as Pick<Props, 'show'> // we need to explicitly pick props we wanna destructure, rest is gonna be type `{}`
      return (
        <Toggleable
          show={show}
          render={renderProps => <UnwrappedComponent {...rest} {...renderProps} />}
        />
      )
    }
  }

  return hoistNonReactStatics(WithToggleable, UnwrappedComponent as any) as ComponentType<Props>
}

Суммировать

При работе с TypeScript и React правильная реализация компонентов, обеспечивающих безопасность типов, может оказаться непростой задачей. Но с новыми функциями, добавленными в TypeScript 2.8, мы можем писать безопасные для типов компоненты почти во всех шаблонах компонентов React.

В этой очень длинной (извините за это) статье, благодаря TypeScript, мы узнали, как писать строго типобезопасные компоненты по множеству шаблонов.

Самым сильным из этих шаблонов должен быть шаблон свойства Render, который позволяет нам реализовать другие распространенные шаблоны, такие как внедрение компонентов, компоненты более высокого порядка и т. д., без слишком больших изменений.

Все демо в тексте можно найти по адресумой репозиторий на гитхабенайти в.

Кроме того, важно понимать, что безопасность типов шаблонов, продемонстрированная в этой статье, может быть реализована только в библиотеках, использующих VDOM/JSX.

  • Шаблоны Angular имеют языковую службу для обеспечения безопасности типов, но простые проверки конструкции, такие как ngFor, похоже, не работают...
  • Шаблоны Vue не похожи на Angular, их шаблоны и привязки данных — это просто волшебные строки (но это может измениться в будущем. Хотя вы можете использовать VDOM в шаблонах, из-за различных типов определений свойств, он использует очень громоздкий (это снибдом ...))

Как всегда, если у вас есть какие-либо вопросы, свяжитесь со мной здесь или в Твиттере (@martin_hotell), и всем удачной проверки шрифтов, ура!

Если вам интересна наша команда, вы можете подписаться на рубрику и подписатьсяgithubИли отправьте свое резюме на tao.qit####alibaba-inc.com'.replace('####', '@'), приглашаем присоединиться людей с высокими идеалами~

Оригинальный адрес:GitHub.com/proto team/no…