[Программирование, ReactJS, TypeScript] Чего мне не хватало в функциональных компонентах React.js
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
За последние годы о 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
===========
Похожие новости:
- [JavaScript, Программирование, Совершенный код] Стек вызовов JavaScript и ещё большая магия
- [Разработка веб-сайтов, JavaScript, Программирование] Создаем Booking приложение с Webix UI
- [Python, Программирование] «За что вы так меня не любите?» (с) Python
- [Python, Программирование, Разработка робототехники, Робототехника] Как установить ROS NOETIC на UBUNTU 20.04
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Программирование — это скучная магия (перевод)
- [Программирование, TDD, Разработка под Android, Kotlin] Пишем unit тесты так, чтобы не было мучительно больно
- [Программирование микроконтроллеров, DIY или Сделай сам] Видеоконтроллер RA8875 и внешние шрифты на EEPROM W25Q32 для быстрого вывода текста на экран дисплея
- [Настройка Linux, Программирование, *nix, DIY или Сделай сам, Автомобильные гаджеты] Чини свою Теслу сам, тыжпрограммист
- [Программирование микроконтроллеров, Научно-популярное, Космонавтика, Мультикоптеры, Будущее здесь] Во время теста лопастей марсианского вертолета «Индженьюити» на высоких оборотах произошла нештатная ситуация
- [Программирование, Java, Разработка под Android, Rust] Rust — теперь и на платформе Android (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_reactjs, #_typescript, #_react.js, #_react_hooks, #_usecallback, #_programmirovanie (
Программирование
), #_reactjs, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:55
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
За последние годы о 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>; } 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>; } 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); } }; } 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; } =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_reactjs, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:55
Часовой пояс: UTC + 5