Русский
Русский
English
Статистика
Реклама

Чего мне не хватало в функциональных компонентах 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

Источник: habr.com
К списку статей
Опубликовано: 12.04.2021 18:13:34
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

Reactjs

Typescript

React.js

React hooks

Usecallback

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru