Комментарии 22
А можно просто воспользоваться MobX:
interface IProps {
store: SomeStore;
}
class SomeStore {
constructor() {
makeObservable(this, {
numValue: observable,
strValue: observable,
increaseNumValue: action,
});
}
numValue = 0;
strValue = "";
increaseNumValue(diff: number) {
this.numValue += diff;
}
}
const SomeComponent = observer((props: IProps) => {
const { store } = props;
useEffect(() => {
const intervalHandle = setInterval(() => store.increaseNumValue(1), 1000);
return () => clearInterval(intervalHandle);
}, [store]);
return <div>
<span>{store.numValue}</span>
<Input type="text" value={store.strValue}
onChange={(ev) => store.strValue = ev.target.value} />
<Button onClick={() => store.numValue = -10}>-10</Button>
</div>;
});
// instead of numChanged, stringChanged
reaction(() => store.numValue, (newVal, oldVal) => {
console.log(`numValue has been changed from ${oldVal} to ${newVal}`);
});
reaction(() => store.strValue, (newVal, oldVal) => {
console.log(`strValue has been changed from ${oldVal} to ${newVal}`);
});
Я думал, что это шутка — комменты про mobx, в тему и не в тему, в каждом посте про react.
P.S.: arrow functions в хендлерах нужно всё-таки оборачивать. И перечитайте статью.
Я думал, что это шутка — комменты про mobx, в тему и не в тему, в каждом посте про react.
Почему в данном случае это не в тему? Он решает именно ту проблему, которую поставил автор. Лучше или хуже — это уже другой вопрос.
P.S.: arrow functions в хендлерах нужно всё-таки оборачивать. И перечитайте статью.
В данном случае не обязательно их использовать. Перечитайте документацию про useCallback и когда его следует использовать.
Ну а ещё проще использовать $mol:
$my_app $mol_page
num?val 0
str?val \
body /
<= Num $mol_view sub / <= num
<= Str $mol_string value?val <=> str?val \
<= Dec $mol_button_minor
title \-1
click?event <=> dec?event null
class $my_app extends $.$my_app {
@ $mol_mem
auto() {
this.num( this.num() + 1 )
return new $mol_after_timeout( 1000, $mol_atom2.current.fresh )
}
dec() {
this.num( this.num() - 10 )
}
}
Но разве не понятно, что автору не нужно как проще? Автору нужно сделать буханку из троллейбуса!
Так какую проблему вы решали? Если родителю нужно знать состояние вашего компонента, ну так вытесните это состояние полностью в родителя. Часто в реальности родителю нужно не только "сигнализировать" о изменении стояния, но и он должен иметь возможность его инициализировать или даже менять. Т.е. Ваш первый вариант станет чем-то таким
export type ReactStateAndSetter<S> = [S, React.Dispatch<React.SetStateAction<S>>];
export interface IProps {
numValueState: ReactStateAndSetter<number>;
strValueState: ReactStateAndSetter<string>;
}
export function SomeComponent({numValueState, strValueState}: IProps) {
const [numValue, setNumValue] = numValueState;
const [strValue, setStrValue] = strValueState;
...
}
Плюс в приложении не будет двух копий одного состояния — в текущем компоненте и в родителе.
Если же прямо хочется передавать только колбэк в пропы, то useReducer
, например, может помочь привязать колбэк к изменению состояния более собрано (в одном месте кода, а не состояние + обертка).
К useCallback() это ситуация вообще никак не относится. Вы вот пишите:
элементыinput
иbutton
заменим компонентамиInput
иButton
, которые потребуют обернуть обработчики событий хукомuseCallback
А почему до замены не нужно было оборачивать? Я сейчас не к тому, зачем вообще useCallback
, я к тому, что это не связанные вещи.
А почему до замены не нужно было оборачивать?
Потому что до этого они не использовались в useEffect или других useCallback.
Прием с использованием useMembers пришел мне в голову на гораздо более сложном примере, когда функциональный компонент раздулся до неприличных размеров, массивы зависимостей у функций вырастали элементов до 7. Я уже отчаялся отладить этот компонент, переписал его в виде класса, отладил, а позже придумал вынести обработчики событий из функционального компонента в методы класса.
К моему удивлению, такого метода нигде в интернете не нашел, поэтому решил поделиться здесь.
А почему до замены не нужно было оборачивать? Я сейчас не к тому, зачем вообще useCallback, я к тому, что это не связанные вещи.
В React компоненты Input и Button следует передавать колбэки, обёрнутые в useCallback, для предотвращения ненужных рендеров (при условии, что эти компонеты обёрнуты в React.memo() либо реализуют shouldComponentUpdate). Для input И button оборачивание в useCallback никакой пользы не принесёт.
deleted
habr.com/ru/post/541884/#comment_22689676
Я эту идею тоже потом использовал в своей недавней статье.
Ваш пример можно упростить.
Вынесите хуки тоже в класс, тогда не придеться передавать зависимости от хуков этого компонента и не нужно будет возвращать функции-эффекты.
По аналогии с setDeps создайте метод, который постоянно вызывается в useMembers и вызывайте в нем ваши хуки. Т.к. этот метод будет вызываться при каждом вызове функционального компонента, то для react он не будет отличаться от custom hook.
class Members extends MembersBase<IDeps> {
// …
onRender = () => {
useEffect(this.intervalEffect, []);
const [numValue, setNumValue] = useState(0);
this.numValueStatePair = {numValue, setNumValue};
};
}
export function useMembers<D, T extends MembersBase<D>>(ctor: (new () => T), deps: (T extends MembersBase<infer D> ? D : never)): T {
// ...
rv.setDeps(deps);
rv.onRender(); // вызов метода
return rv;
}
Да и для компонентов-классов можно написать обертки над методами жизненных циклов и setState, чтобы их задавать в компоненте-классе по аналогии с useEffect, useState. Правда useState не будет так же красиво выглядеть.
Ваш пример можно упростить.
Вынесите хуки тоже в класс, тогда не придется передавать зависимости от хуков этого компонента и не нужно будет возвращать функции-эффекты.
Если я всё правильно понял, вы предлагаете писать так:
interface IMembers<T> {
onRender(props: T): JSX.Element;
}
function useMembers<P, T extends IMembers<P>>(ctor: (new () => T), props: P) {
const ref = useRef<T>();
if (!ref.current) {
ref.current = new ctor();
}
ref.current.onRender(props);
}
export function SomeComponent(props: IProps) {
return useMembers(Members, props);
}
class Members extends IMembers<IProps> {
...
private deps: IDeps;
onRender = (props: IProps) => {
const [numValue, setNumValue] = React.useState(0);
const [strValue, setStrValue] = React.useState("");
this.deps = { props, numValue, setNumValue, setStrValue };
React.useEffect(this.intervalEffect, []);
return <div>
<span>{`Число = ${numValue}`}</span>
<Input type="text" onChange={this.onTextChanged} value={strValue} />
<Button onClick={this.onBtnClick}>-10</Button>
</div>;
}
...
}
Функция компонента опустела, код полностью переехал в класс. Что мы выиграли? Отказались от одной операции деструктуризации объекта
const { onTextChanged, onBtnClick, intervalEffect } = useMembers(...
, добавив префиксы this к названиям функций. Перенесли объявление private deps: IDeps;
в реализацию класса.На мой взгляд — сомнительное упрощение. Читаемость кода ухудшилась. Я предпочту остаться в парадигме функционального компонента: хуки в основной функции, перенося в класс только громоздкие функции с зависимостями.
А за комментарий спасибо.
Согласен с минусами переноса хуков в класс. Показалось, что если в данном случае отделить логику от представления, то получится лучше.
Собственно, я предложил перенести хуки в класс, т.к. в коде из статье мне не очень понравилось разделение эффекта на вызов через useEffect и его объявление в классе. С одной стороны это гибче, а с другой стороны нет информации о том, как его использовать в компоненте. Для этого нужно посмотреть его реализацию. К тому же реализация может измениться, но кто-нибудь забудет изменить код его использования в useEffect. Т.е. возможны 3 варианта использования:
useEffect(intervalEffect);
useEffect(intervalEffect, []);
useEffect(intervalEffect, [x, y]);
но 2 из них скорее всего будут ошибочными.
Предлагаю выносить загромождающие код обработчики в объект класса вместе с зависимостями. Разве так не лучше?
Довольно странно видеть, как автор попробовав путь функциональных хуков вернулся, пусть и не явно, к использованию привычного набора методов объеденных в класс, получив все те недостатки, что были в классовых React компонентах и проигнорировав все те преимущества, что несут функциональные React хуки.
Предлагаю свой вариант приведенного выше синтетического примера в попытке продемонстрировать идею функциональных хуков. Первое, что нам понадобиться это вынести общий код в небольшие функции хелперы, достаточно простые и переиспользуемые:
1. Хук позволяющий вызвать коллбэк с заданным интервалом:
function useInterval<T extends () => void>(cb: T, timeout = 1000) {
useEffect(() => {
const intervalHandle = setInterval(cb, timeout);
return () => clearInterval(intervalHandle);
}, [cb, timeout]);
}
2. Хук позволяющий хранить некое значение на изменение которого можно подписаться:
function useValue<V>(
initValue: V,
onValueChange?: (nextValue: V) => void
): [V, (nextValue: V | ((prevValue: V) => V)) => void] {
const [value, setValue] = useState(initValue);
useEffect(() => {
if (onValueChange) {
onValueChange(value);
}
}, [value, onValueChange]);
return [value, setValue];
}
3. Хук создающий обработчик для input элементов:
function useInputHandler(handler: (value: string) => void) {
return useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
handler(e.target.value);
},
[handler]
);
}
4. Хук создающий обработчик изменяющий значение на заданную величину:
function useNumHandler(
diff: number,
setNumValue: (value: (prevValue: number) => number) => void
) {
return useCallback(() => {
setNumValue((value) => value + diff);
}, [diff, setNumValue]);
}
Еще раз обращаю внимание что у нас появилось 4 небольших хука с вполне понятной и законченной логикой, которые можно переиспользовать в любых компонентах и протестировать независимо друг от друга. Например, мы можем их использовать для рефакторинга приведенного в статье кода:
interface IProps {
numChanged?: (sum: number) => void;
stringChanged?: (concatRezult: string) => void;
}
export function SomeComponent(props: IProps) {
const { numChanged, stringChanged } = props;
const [numValue, setNumValue] = useValue<number>(0, numChanged);
const [strValue, setStrValue] = useValue<string>("", stringChanged);
const onTextChanged = useInputHandler(setStrValue);
const incNumByOne = useNumHandler(1, setNumValue);
const decNumByTen = useNumHandler(-10, setNumValue);
useInterval(incNumByOne);
return (
<div>
<span>{numValue}</span>
<input type="text" onChange={onTextChanged} value={strValue} />
<button type="button" onClick={decNumByTen}>
-10
</button>
</div>
);
}
Ну и в заключении, никто не говорит, что код на хуках лучше или хуже классов, выбирайте то что вам по душе и используйте с умом.
const { numChanged, stringChanged } = props;
const [numValue, setNumValue] = useValue<number>(0, numChanged);
const [strValue, setStrValue] = useValue<string>("", stringChanged);
const onTextChanged = useInputHandler(setStrValue);
const incNumByOne = useNumHandler(1, setNumValue);
const decNumByTen = useNumHandler(-10, setNumValue);
useInterval(incNumByOne);
let num = 0
let str = ""
const onTextChanged = v => str = v
const incNumByOne = ()=> num += 1
const decNumByTen = ()=> num -= 10
setInterval(incNumByOne, 100);
Где-то мы пропустили поворот..
А ещё useValue сигнализирует об изменении value при изменении ссылки на onValueChange. И редко кто задумывается об этом. Приходится костылить с всякими useBoundCallback, чтобы иметь стабильную ссылку на callback который будет в актуальном замыкании.
Чего мне не хватало в функциональных компонентах React.js