[Программирование, ReactJS, TypeScript] Чего мне не хватало в функциональных компонентах React.js

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
12-Апр-2021 20:31

За последние годы о 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
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_reactjs, #_typescript, #_react.js, #_react_hooks, #_usecallback, #_programmirovanie (
Программирование
)
, #_reactjs, #_typescript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 12:55
Часовой пояс: UTC + 5