Профиль переводчика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 сразу сообщит нам, что мы делаем что-то не так.
Пример: состояние безопасности типов во время компиляции
Реализация всего компонента контейнера/компонента с отслеживанием состояния:
В нашем компоненте-контейнере еще нет 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
был определен в:
Чтобы удовлетворить компилятор 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
}
Использование компонентов остается прежним:
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>
);
}
Допустим, вам нужно создать расширяемый компонент меню, который должен отображать дочерний контент, когда пользователь нажимает на него. Для этого мы можем использовать различные шаблоны компонентов.
визуализировать режим обратного вызова/рендеринга свойств
Лучший способ добиться повторного использования логики компонента — поместить дочерние элементы компонента в функции или использоватьrender
API свойств. Вот почему обратные вызовы рендеринга также называются подкомпонентами функций.
Давайте реализуем метод свойства рендеринга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 и правильная проверка типов для параметров свойства рендеринга:
Если мы хотим использовать его повторно (например, в нескольких компонентах меню), нам просто нужно создать компонент сердца, использующий логику 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>
</>
);
}
}
и это также работает, как мы ожидаем:
Этот шаблон очень полезен, когда мы хотим изменить визуализируемый контент, но не заботимся об изменении состояния: как видите, мы переместили логику рендеринга в дочернюю функцию компонента 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 }
:
Мы все еще можем использовать то же самое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={}
Свойства имеют правильную проверку типов. Подарите ему аплодисменты!
компоненты более высокого порядка
Поскольку мы создали функцию обратного вызова рендеринга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)
Все работает нормально, плюс проверка безопасности типа! отличный!
Управляемые компоненты
Это последний шаблон компонента! Предположим, мы хотим контролировать нашуToggleable
компоненты нам нужныToggleable
Конфигурация компонента. Это очень мощный режим. Давайте сделаем это.
Когда я говорю контролируемый компонент, что я имею в виду? я хочу отMenu
Таким образом, элемент управления внутри компонентаToggleableManu
Отображается ли содержимое компонента.
Нам нужно обновить наш, как это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 управляемым компонентом, нам нужно:
- Добавить к
show
приписыватьProps
на API - Обновите свойства по умолчанию (поскольку показать необязательно)
- Обновите начальное состояние компонента из Props.show, ведь теперь значение в нашем состоянии может зависеть от свойств, переданных из родительского компонента
- Обновление состояния из реквизита в функции жизненного цикла 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
свойств и обновить нашиOwnProps
API
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…