Чего мне не хватало в функциональных компонентах React.js

от автора

За последние годы о React hooks не писал разве что ленивый. Решился и я.

Помню первое впечатление — WOW-эффект. Можно не писать классы. Не нужно описывать тип состояния, инициализировать состояния в конструкторе, теснить всё состояние в одном объекте, помнить о том, как setState сливает новое состояние со старым. Не придется больше насиловать методы componentDidMount и componentWillUnmount запутанной логикой инициализации и освобождения ресурсов.

Вот простой компонент: управляемое текстовое поле и счетчик, который увеличивается на 1 по таймеру и уменьшается на 10 по нажатию кнопки;

import * as React from "react";  interface IState {     numValue: number;     strValue: string; }  export class SomeComponent extends React.PureComponent<{}, IState> {          private intervalHandle?: number;      constructor() {         super({});         this.state = { numValue: 0, strValue: "" };     }      render() {         const { numValue, strValue } = this.state;         return <div>             <span>{numValue}</span>             <input type="text" onChange={this.onTextChanged} value={strValue} />             <button onClick={this.onBtnClick}>-10</button>         </div>;     }      private onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) =>  				this.setState({ strValue: e.target.value });      private onBtnClick = () => this.setState(({ numValue }) => ({ numValue: numValue - 10 }));      componentDidMount() {         this.intervalHandle = setInterval(             () => this.setState(({ numValue }) => ({ numValue: numValue + 1 })),             1000         );     }      componentWillUnmount() {         clearInterval(this.intervalHandle);     } } 

превращается в ещё более простой:

import * as React from "react";  export function SomeComponent() {     const [numValue, setNumValue] = React.useState(0);     const [strValue, setStrValue] = React.useState("");      React.useEffect(() => {         const intervalHandle = setInterval(() => setNumValue(v => v - 10), 1000);         return () => clearInterval(intervalHandle);     }, []);      const onBtnClick = () => setNumValue(v => v - 10);     const onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => setStrValue(e.target.value);      return <div>         <span>{numValue}</span>         <input type="text" onChange={onTextChanged} value={strValue} />         <button onClick={onBtnClick}>-10</button>     </div>; }

Функциональный компонент не только в два раза короче, он понятнее: функция умещается в один экран, всё перед глазами, конструкции лаконичны и ясны. Красота.

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

interface IProps {     numChanged?: (sum: number) => void;     stringChanged?: (concatRezult: string) => void; }  export function SomeComponent(props: IProps) {     const { numChanged, stringChanged } = props;     const [numValue, setNumValue] = React.useState(0);     const [strValue, setStrValue] = React.useState("");      const setNumValueAndCall = React.useCallback((diff: number) => {         const newValue = numValue + diff;         setNumValue(newValue);         if (numChanged) {             numChanged(newValue);         }     }, [numValue, numChanged]);      React.useEffect(() => {         const intervalHandle = setInterval(() => setNumValueAndCall(1), 1000);         return () => clearInterval(intervalHandle);     }, [setNumValueAndCall]);      const onBtnClick = React.useCallback(         () => setNumValueAndCall(- 10),         [setNumValueAndCall]);      const onTextChanged = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {         setStrValue(e.target.value);         if (stringChanged) {             stringChanged(e.target.value);         }     }, [stringChanged]);      return <div>         <span>{numValue}</span>         <Input type="text" onChange={onTextChanged} value={strValue} />         <Button onClick={onBtnClick}>-10</Button>     </div>; } 

Некрасиво: useCallback уродует код, приходится следить за списком зависимостей. Во избежание дублирования я вынес общий код из обработчика onBtnClick и useEffect в функцию setNumValueAndCall, которую также обернул useCallback, и далее опирался на её (setNumValueAndCall) экземпляр как на зависимость. Возможно, зависимость функции от функции — не лучшее решение, но поставить в зависимость onBtnClick и useEffect список зависимостей setNumValueAndCall тоже наглядностью не выделяется.

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

А классовый компонент переносит тоже расширение функциональности без осложнений.

export class SomeComponent extends React.PureComponent<IProps, IState> {     private intervalHandle?: number;     constructor() {         super({});         this.state = { numValue: 0, strValue: "" };     }      render() {         const { numValue, strValue } = this.state;         return <div>             <span>{numValue}</span>             <Input type="text" onChange={this.onTextChanged} value={strValue} />             <Button onClick={this.onBtnClick}>-10</Button>         </div>;     }      private onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => {         this.setState({ strValue: e.target.value });         const { stringChanged } = this.props;         if (stringChanged) {             stringChanged(e.target.value);         }     }      private onBtnClick = () => this.setNumValueAndCall(- 10);      private setNumValueAndCall(diff: number) {         const newValue = this.state.numValue + diff;         this.setState({ numValue: newValue });         const { numChanged } = this.props;         if (numChanged) {             numChanged(newValue);         }     }      componentDidMount() {         this.intervalHandle = setInterval(             () => this.setNumValueAndCall(1),             1000         );     }      componentWillUnmount() {         clearInterval(this.intervalHandle);     } } 

Что же делать? В сложных случаях возвращаться к компонентам-классам? Ну уж нет, нам слишком нравятся возможности, привнесенные хуками.

Предлагаю выносить загромождающие код обработчики в объект класса вместе с зависимостями. Разве так не лучше?

export function SomeComponent(props: IProps) {     const [numValue, setNumValue] = React.useState(0);     const [strValue, setStrValue] = React.useState("");     const { onTextChanged, onBtnClick, intervalEffect } =            useMembers(Members, { props, numValue, setNumValue, setStrValue });      React.useEffect(intervalEffect, []);      return <div>         <span>{numValue}</span>         <Input type="text" onChange={onTextChanged} value={strValue} />         <Button onClick={onBtnClick}>-10</Button>     </div>; }  type SetState<T> = React.Dispatch<React.SetStateAction<T>>;  interface IDeps {     props: IProps;     numValue: number;     setNumValue: SetState<number>;     setStrValue: SetState<string>; }  class Members extends MembersBase<IDeps> {      intervalEffect = () => {         const intervalHandle = setInterval(() => this.setNumValueAndCall(1), 1000);         return () => clearInterval(intervalHandle);     };      onBtnClick = () => this.setNumValueAndCall(- 10);      onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => {         const { props: { stringChanged }, setStrValue } = this.deps;         setStrValue(e.target.value);         if (stringChanged) {             stringChanged(e.target.value);         }     };      private setNumValueAndCall(diff: number) {         const { props: { numChanged }, numValue, setNumValue } = this.deps;         const newValue = numValue + diff;         setNumValue(newValue);         if (numChanged) {             numChanged(newValue);         }     }; } 

Код компонента снова прост и изящен. Обработчики событий вместе с зависимостями мирно ютятся в классе.

Хук useMembers и базовый класс тривиальны:

export class MembersBase<T> {     protected deps: T;     setDeps(d: T) {         this.deps = d;     } }  export function useMembers<D, T extends MembersBase<D>>(ctor: (new () => T), deps:  (T extends MembersBase<infer D> ? D : never)): T {     const ref = useRef<T>();     if (!ref.current) {         ref.current = new ctor();     }     const rv = ref.current;     rv.setDeps(deps);     return rv; } 

Код на Github

ссылка на оригинал статьи https://habr.com/ru/post/552046/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *