Как стать автором
Обновить

Комментарии 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)
Возможно себе возьмем на вооружение какие то подходы)
Спасибо!)
мы его и используем)
Как бы вот так надо, зачем лишние манипуляции и лишний быдло-код? =)
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;
}
Тоже подумывал об отделение логики от View в функциональных компонентах. В компонентах на классах с этим было проще.
Спасибо за интересный пример! Он демонстрирует широкие возможности для написания функциональных компонентов с более понятным и предсказуемым потоком управления. Осталась дождаться, когда 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'и. Чтобы обеспечить представление необходимыми интерфейсами, приходилось обертывать несколько раз. Неудобно, негибко. Думаю, тут не надо подробно расписывать.

Вы же не считаете, что аналоги декораторов в виде HOC — это предел совершенства композиции в ООП и что авторы React первые, кто столкнулся с тем, что надо предоставить возможность разработчикам расширять что-то подобное компонентам React и в других областях не существует других решений, которые авторам React не известны? Есть, просто во фронтенде один юный разработчик умудрился переключить внимание почти всего сообщества с объектов на функции и перестали искать другие решения для компонентов-объектов.
Как я уже упоминал, скоро
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.

Скину потом сюда ссылку.
Я на связанную с этим тему планирую выложить пару статей, где описываются другие подходы повторного использования кода, а не только наследование и декораторы.

Интересно будет взглянуть.

Выложил статьи. Вот только получились довольно большие и, наверное, мало кто прочитает.
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 программирование с использованием хуков. Оно подразумевает:

— больше React, чем JavaScript
— накладные расходы в пользу большей декларативности

> при вызове handleClick будет изменяться state компонента, а это значит будет происходить полный рендер компонента, только для того чтобы обнулить timeout звучит дорого

Если для вашего проекта это дорого, то вы либо не должны использовать React в принципе, либо занимаетесь преждевременной оптимизацией.
Я думаю здесь конфликтуют две идеологии, в основе которых мы принимаем решения, писать код так или иначе.

Я не считаю вашу идеологию плохой, но мне ближе писать в первую очередь на JS чем на React, т.к. это лишь библиотека для отображения :)
Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.

Они сейчас посмотрят на этот код, скопипастят, поменяют setTimeout на setInterval, и получат ай-яй-яй. И потом окажется, что useRef для управления ресурсами не очень то и подходит.

React, т.к. это лишь библиотека для отображения


А вы не путаете React и JSX? useState, useEffect, useMemo, useRef, useCallback, useReducer не имеют прямого отношения к отображению.

Я пишу сейчас 3D редактор на React, где для отображения используется Babylon.js. React используется как декларативная state machine и для управления ресурсами. Куча React кода, а из отображения там только
<canvas ref={setCanvasRef} />
> Я бы не учил новичков мешать разные подходы. У них и так часто каша в голове. Статья должна либо учить React и React-way, либо учить plain JavaScript. Это моё мнение.

React написан на JS как тут можно их не мешать я не очень представляю)
Да и контент который я публикую, он никак не направлен на новичков. Для новичков и так курсов / уроков / статей пруд пруди. А вот для senior разрабов контента порассуждать не хватает. Собственно поэтому я и начал свою деятельность.

Спор дальнейший не вижу особого смысла продолжать :) очевидно что мы оба останемся при своем мнении)
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории