Комментарии 23
В рефах хорошо хранить вью-модели, оборачивая их создание в пользовательский хук. Таким образом выносим логику из представления (не придется городить лапшу на хуках), а модель получает доступ к жизненному циклу компонента, в частности, очистку перед сносом.
а что включает ваша viewModel?
onClick обработчики включает? как значение там обновляете, если к примеру redux-store обновился и прочее) если выложите черновик где-нибудь, было бы круто!
Вот примерный набросок для MobX
// использование мобиксового стора в компоненте
const MyComponent = observer(props => {
const model = useMyViewModel();
return (
<div onClick={model.handleClick}>
{model.text}
</div>
);
})
// вью-модель - мобиксовый стор
class MyViewModel {
constructor(...) { ... }
@observable text = '123';
@action handleClick = (e) => {
// что-то делаем
};
clear = () => {
/* тут можно какую-нибудь очистку,
например убиение таймеров */
};
}
// хук-поставщик для модели
function useMyViewModel() {
return useViewModel(() => new MyViewModel())
}
// вспомогательный хук для создания вью-моделей, с опцией очистки
function useViewModel(creator) {
const ref = useRef();
if (!ref.current) {
ref.current = creator();
}
useEffect(() => () => {
if (ref.current && typeof ref.current.clear === 'function') {
ref.current.clear();
}
}, []);
}
забыл "return ref.current;" в последней функции
мы тоже пишем на mobX, но с хуками мучаемся во ViewModel)
Возможно себе возьмем на вооружение какие то подходы)
Спасибо!)
В принципе, у мобикса из коробки есть что-то похожее: https://github.com/mobxjs/mobx-react#uselocalobservable-hook
const MyComponent = observer(props => {
const [model] = useState(() => new MyViewModel());
useEffect(() => {
return model.clear; // cleanup
});
return (
<div onClick={model.handleClick}>
{model.text}
</div>
);
})
// вью-модель - мобиксовый стор
class MyViewModel {
constructor(...) { ... }
@observable text = '123';
@action handleClick = (e) => {
// что-то делаем
};
clear = () => {
/* тут можно какую-нибудь очистку,
например убиение таймеров */
};
}
Вьюха засоряется лишним кодом, который к ней не относится (нарушение SRP)
Если оную модель захочется поюзать ещё где, придется копипастить всю логику (нарушение DRY)
Сильная зацепленность, вьюха должна знать, что модели требуется очистка, и что модель создается конструктором, и вообще что это вью-модель, а не обычная. Эти знания не нужны для функциональности вьюхи.
Ещё до кучи нарушение DIP.
В общем, одни плюсы :)
const MyComponent = observer(props => {
const model = useLocalState(() => new MyViewModel());
return (
<div onClick={model.handleClick}>
{model.text}
</div>
);
})
// вью-модель - мобиксовый стор
class MyViewModel {
constructor(...) { ... }
@observable text = '123';
@action handleClick = (e) => {
// что-то делаем
};
clear = () => {
/* тут можно какую-нибудь очистку,
например убиение таймеров */
};
}
function useLocalState(creatorFn) {
const [model] = useState(creatorFn);
useEffect(() => {
return typeof model.clear === 'function' && model.clear; // cleanup
});
return model;
}
Спасибо за интересный пример! Он демонстрирует широкие возможности для написания функциональных компонентов с более понятным и предсказуемым потоком управления. Осталась дождаться, когда MobX переплюнет Redux, разделение логики и View в компоненте станет стандартом и затем компоненты снова переделают на классы или объекты)
Не вижу необходимости в хуке useMyViewModel. ViewModel в нем заменить нельзя, а его нельзя заменить компоненте. Можно сразу класс передать в хук useViewModel.
Подумал также, что подход из вашего примера по аналогии с custom hooks позволяет повторное использование ViewModel и их взаимодействие в одном компоненте при необходимости:
const MyComponent = observer(props => {
const modelA = useMyViewModel(MyViewModelA);
const modelB = useMyViewModel(MyViewModelB, modelA); //передача экземпляра первой ViewModel во вторую, если это где-то понадобится.
return (
<div onClick={modelA.handleClick}>
{modelA.text}
</div>
);
})
и затем компоненты снова переделают на классы или объекты)
Ну это вряд ли, эволюция показала, что функциональные компоненты всё-таки лучше. ООП идеально для логики, вот и будет отдельно. А компоненты-классы, это боль, боль, боль… Сейчас вот разгребаю по работе, увы. С каждым днём всё яснее видится мне величие принципа SRP..
Не вижу необходимости в хуке useMyViewModel.
Да нет, это важный момент. На самом деле сейчас подумал, что лишнее тут только слово View. Надо useMyModel. По функциональности, в общем. То что это временная модель, компоненту-пользователю модели не интересно.
"Персональный" хук-поставщик (причем даже не для конкретного класса, а для интерфейса) позволяет уменьшить зацепленность. Можно менять жизненный цикл у модели, можно резолвить некоторые её зависимости, не меняя использующий код, можно создавать экземпляры подклассов при некоторых условиях. В общем, идеальное связующее звено.
эволюция показала, что функциональные компонентыНу вы же в итоге используете классы внутри функционального компонента.
Это разве сильно отличается от идеи сделать компонент объектом/классом?
Согласен, что функции идеальны для JSX кода.
Но и над компонентами-классами можно подшаманить и функцию render выносить отдельно и применять к любому компоненту, как и разбить компонент на составляющие, которые также можно применять к любому компоненту.
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.
Насчет персонального хука-поставщика.
Если где-то нужно добавить некую логику между компонентом и моделью, то да, его стоит добавлять. Может местами даже цепочку хуков.
Не увидел смысла, когда он показан как в примере, где он ничего не делает и не отделим от класса модели.
Ну вы же в итоге используете классы внутри функционального компонента. Это разве сильно отличается от идеи сделать компонент объектом/классом?
Это совершенно разные вещи. Компонент остается функцией, а использует, повторюсь, не классы, а решателей определенных задач.
Но и над компонентами-классами можно подшаманить и функцию render выносить отдельно и применять к любому компоненту, как и разбить компонент на составляющие, которые также можно применять к любому компоненту.
Было уже. Итогом развития этой идеи стали HOC'и. Чтобы обеспечить представление необходимыми интерфейсами, приходилось обертывать несколько раз. Неудобно, негибко. Думаю, тут не надо подробно расписывать.
Как я уже упоминал, скоро
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.
Скину потом сюда ссылку.
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.
Интересно будет взглянуть.
habr.com/ru/post/545368
C 3-го пункта (c паттерна «Стратегия») и ниже описаны подходы, которые не распространены в React.
Во второй статье я описал применение одного из подходов в React компонентах (для компонентов-классов и компонентов-функций).
habr.com/ru/post/545064
Для реализации его в функциональных компонентах я использовал ваши идеи с userRef и пробросом событий из хуков. Еще раз благодарю за интересный и полезный пример!
А где же тогда хранить timeoutId?
В большинстве случаев он хранится в замыкании функции очистки.
const Test = () => {
const [isTimeoutActive, setIsTimeoutActive] = useState(true);
const handleClick = () => setIsTimeoutActive(false);
useEffect(() => {
if (!isTimeoutActive) return;
const timeoutId = setTimeout(() => {
// do some action
}, 3000);
return () => {
clearTimeout(timeoutId);
};
}, [isTimeoutActive]);
return (...);
}
Приимущества:
- Декларативность, ради которой хуки и создавались
- Код менее размазан. Создание и удаление ресурса находятся рядом.
- Меньше возможностей у компонента привратится спагетти-код (например начав присваивать в timeout.current какие-то другие таймауты)
- Таймаут автоматически отменится, когда компонент перестанет существовать
Но я бы делать так не стал по нескольким причинам:
— при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого
— читабельность тоже как по мне не особо улучшилась, теперь в этом коде стало гораздо React чем JavaScript. Чтобы обнулить timeout мы вызываем не clearTimeout, а вызываем какую-то функцию абстракцию, на которую где-то в другом месте подписан useEffect, который в return возвращает функцию, которая обнулит timeout. По мне так, этот вариант и есть спагетти когда одно за другое зацепленно, как макарошки вокруг вилки
— больше React, чем JavaScript
— накладные расходы в пользу большей декларативности
> при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого
Если для вашего проекта это дорого, то вы либо не должны использовать React в принципе, либо занимаетесь преждевременной оптимизацией.
Они сейчас посмотрят на этот код, скопипастят, поменяют setTimeout на setInterval, и получат ай-яй-яй. И потом окажется, что useRef для управления ресурсами не очень то и подходит.
React, т.к. это лишь библиотека для отображения
А вы не путаете React и JSX? useState, useEffect, useMemo, useRef, useCallback, useReducer не имеют прямого отношения к отображению.
Я пишу сейчас 3D редактор на React, где для отображения используется Babylon.js. React используется как декларативная state machine и для управления ресурсами. Куча React кода, а из отображения там только
<canvas ref={setCanvasRef} />
React написан на JS как тут можно их не мешать я не очень представляю)
Да и контент который я публикую, он никак не направлен на новичков. Для новичков и так курсов / уроков / статей пруд пруди. А вот для senior разрабов контента порассуждать не хватает. Собственно поэтому я и начал свою деятельность.
Спор дальнейший не вижу особого смысла продолжать :) очевидно что мы оба останемся при своем мнении)
Что выбрать: глобальные переменные или useThis?