Обновить
29
Егор Горбачёв@kubk

Фулстек-разработчик

9
Подписчики
Отправить сообщение
Геттер, который заменяет вычисляемое значение. Его логика работы отличается от Vue
Вы тут умолчали о том, что в примере на Vue у вас есть мемоизация и автоматическое вычисление зависимостей (благодаря Transparent Reactive Programming). В примере на Angular функция будет пересчитываться на каждый вызов Change Detection, что гораздо менее эффективно. Не говоря уже о том, что во Vue вам не нужно делать подписку на интервал вне Zone.js, а в Angular вы рано или поздно столкнётесь с необходимостью так делать. Я как-то добавлял таймер обратного отсчёта в Angular приложение, таймер был вверху дерева компонентов и на каждый тик триггерил CD рекурсивно у всех компонентов ниже. Тут ещё спасает OnPush, но во Vue это не нужно.
Он использует библиотеку для реактивного программирования RxJS, без которой также обойтись нельзя.
Без RxJS можно прекрасно обойтись даже в Angular, посмотрите например Mobx-Angular. Я был на проекте, который писали фулстеки с уклоном в бекенд, и им было очень тяжело совладать с RxJS и его операторами, в коде были все возможные антипаттерны RxJS — вложенные подписки, отсутствие отписок, непонимание разницы между switchMap/mergeMap/concatMap и как следствие трудновоспроизводимые баги. Mobx зашёл на ура разработчикам из-за привычной модели программирования, писать код стало проще. Ну и вот возьмём ваш пример на RxJS, в нём ведь тоже есть проблемы:
— Лишний BehaviorSubject. Вместо того, чтобы считать количество кликов через оператор scan вы ввели состояние, которое мутируете вручную. Это императивный подход. Такой код люди пишут постоянно, потому что ломать голову операторами RxJS сложно.
— Подписка на интервал в шаблоне у вас скорее всего через async pipe, а это опять будет триггерить CD на каждый тик. На интервалы/таймеры нужно подписываться вне Zone.js

Вся эта ненужная сложность, отсутствие девтулзов и ещё вагон проблем с типизацией (она в Angular слишком щадящая) и побудили меня отказаться от этого фреймворка в пользу React.
Ну и хорошо, что наследование не будет работать, вместо него лучше композиция: en.wikipedia.org/wiki/Composition_over_inheritance
Необходимость помнить о потере this — лишняя когнитивная нагрузка, а бесконечный поток статей про контекст в JS тому подтверждение.
Всё-таки constate не решает проблему Provider hell — обилие провайдеров, которые должны быть в определённом порядке, чтобы хуки могли зависеть друг от друга. Библиотека reusable выглядит более продуманной: github.com/reusablejs/reusable
Одно из преимуществ использования хуков для управление состоянием — возможность идти в ногу с экосистемой реакта. Можно использовать популярные хуки вроде useMedia, useLocation, useQuery вне компонента, лучше отделяя логику от UI. Другие стейт-менеджеры предлагают писать свои обёртки вместо использования готовых хуков, так как готовые хуки не дружат с их системой реактивности (пример для Effector).
Другой вариант — в mobx-react-lite есть хук useLocalStore, который позволяет совмещать хуки с удобством Mobx (иммутабельность, точечные обновления UI, вычисляемые значения).
А что за менеджер состояния, если не секрет? Как у вас организована валидация и каковы причины отказа от библиотек для форм? Сам пребываю в поисках model-driven библиотеки для управления формами, так как большинство существующих нельзя использовать в отрыве от UI.
А что там удобного? Возможность использовать JS-переменные при формировании темы?
Для отмены и повтора есть opinionated Mobx — Mobx State Tree. Не говоря уже о том, что паттерн Команда без проблем реализуется вручную независимо от стейт-менеджера: en.wikipedia.org/wiki/Command_pattern
Как раз в Redux путешествие по истории реализовано максимально неэффективно — запоминаются N последних состояний, вместо того, чтобы запоминать только разницы между состояниями. Представьте, что если бы Git хранил коммиты не как diff'ы от начального состояния, а как промежуточные состояния с большим количеством дублирования. Это именно то, что предлагает документация Redux: redux.js.org/recipes/implementing-undo-history#second-attempt-writing-a-reducer-enhancer
В Mobx State Tree напротив можно хранить историю патчей состояния, а не сами состояния: mobx-state-tree.js.org/concepts/patches
Справедливее было бы назвать статью «Число обнаруженных багов увеличилось». Баги-то могли быть допущены в более ранних версиях софта, а обнаружили только сейчас. Сомневаюсь, что для каждого бага искали породивший его коммит и проверяли его дату.
Так и есть, тому масса подтверждений. С 2016-го года висят открытые issue, которые просто игнорируются:
— Нетипизированные формы: github.com/angular/angular/issues/13721
— Нельзя использовать spread-оператор в шаблонах: github.com/angular/angular/issues/11850
— Нельзя использовать стрелочные функции в шаблонах: github.com/angular/angular/issues/14129
— Проблемы со стилями в директивах: github.com/angular/angular/issues/17766
Проблем на самом деле больше, это те, что сразу вспомнились. Не припомню такие ситуации в мире реакта.
Проект ~50k строк, за полгода почти закончили миграцию с NGRX на Mobx. В некоторых PR при замене NGRX слайса на Mobx стор кода удалялось раза в 3 больше, чем добавлялось. Появилась возможность легко покрывать тестами сторы и легко мокать сторы для компонентов в сторибуке — нужно просто подменить сервис через DI контейнер. Пишу большую статью про Mobx, результатом очень доволен.
Судя по документации, в NGXS присутствуют все проблемы Redux-подобных сторов: неудобная иммутабельность, необходимость вручную описывать мемоизированные селекторы, обилие бойлерплейт кода. Вот пример из документации:

export class FeedZebra {
  static readonly type = '[Zoo] FeedZebra';
  constructor(public zebraToFeed: ZebraFood) {}
}

...

@Action(FeedZebra)
feedZebra(ctx: StateContext<ZooStateModel>, action: FeedZebra) {
  const state = ctx.getState();
  ctx.patchState({
    zebraFood: [
      ...state.zebraFood,
      action.zebraToFeed,
    ]
  });
}

И это всё нужно для того, чтобы запушить значение в массив? На Mobx будет так:

@action      
feedZebra(zebraToFeed: ZebraFood) {
  this.zebraFood.push(zebraToFeed)
}


Mobx в каждом observable значении (в нашем случае zebraFood) хранит список слушателей, которые будут отработать при изменении этого значения. Слушателями будут выступать компоненты, которые рендерят этот observable. На выходе очень малое количество кода и прозрачный принцип работы. Чуть более подробно о недостатках Redux-подобных сторов, писал тут: habr.com/ru/company/inobitec/blog/481288/#comment_21052464
После es2015 у вас будет ~240кб, а не 60. У Xuxicheta, видимо, очень большой проект. Правильнее было бы называть разницу в процентах. Вот какие бандлы генерирует один из моих проектов на Angular 9:

image

Ну и с Ангуляром ваш бандл никогда не будет весить 260кб, вот что показывает анализатор бандла у меня в проекте:

image

Тут из 566кб нужно вычесть 126кб angular/cdk и 8 angular/firebase, итого получаем 432кб.
На async пайпах тоже можно писать производительный код, но гораздо сложнее — нужно не забывать правильно расставлять операторы distinctUntilChanged и share. Тут более подробное сравнение. Пример на RxJS:
const displayStream$ = combineLatest(this.nickname$, this.firstname$, this.lastname$)
  .pipe(
      map(([nickname, firstname, lastname]) => nickname ? nickname : firstname + " " + lastname),
      distinctUntilChanged()
  )

Аналогичный пример на Mobx:

@computed get displayname() {
   return this.nickname ? this.nickname : this.firstname + ' ' + this.lastname
}

Второй пример проще и не требует вручную указывать зависимости вычисляемого значения — зависимости вычисляются автоматически. Mobx запомнит зависимости displayname и пересчитает его тогда, когда вызванные внутри геттера observable значения поменяются. Отвечая на ваш вопрос — на RxJS тоже можно писать код с минимальным количеством ререндеров компонента, но на Mobx это делать проще, так как меньше возможностей ошибиться.
Вот пример миграции — habr.com/ru/post/468063
По опыту работы над разными Angular проектами скажу, что писать плохо можно на любом фреймворке и Angular тут не лучше React. Да, есть DI, но неопытный разработчик будет инжектить по 9 сервисов в компонент и фреймворк это не запретит. В одном проекте нормальной практикой были раздутые компоненты по 600-700 строк TS + столько же HTML, отсутствие отписок и очень плохая типизация всего. Ангуляр по умолчанию использует максимально щадящий режим TS и позволяет программировать толком без типов. В create-react-app по умолчанию выставлен strict: true, который, по моему опыту, стараются не выключать. Вывод — в долгосрочной перспективе нужно выбирать не фреймворк, а людей.
Правильно ли понимаю, что с отсутствием node_modules я потеряю возможность читать исходники зависимостей, установленных локально?
А как вы мокаете зависимости в тестах? Через monkey patch импортов? Передача зависимостей в конструктор стора упрощает юнит-тестирование и явно даёт понять, что требуется стору для функционирования. Если вы про DI-контейнер, то конечно же без него можно обойтись и прописывать зависимости ручками.
Можно не делать класс финальным, тогда и проблем с моком не будет. Для запрета наследования можно использовать линтер и статический анализатор. В результате компромисс — новичок доволен, потому что не нужно выделять абстракции в каждом модуле, а тимлид доволен, потому что код по-прежнему тестируемый и наследования в коде нет. Мокать внешние зависимости действительно не следует, но как я понял из статьи, вы предлагаете выделять интерфейсы для всех зависимостей, которые замедляют тесты и взаимодействуют с внешним миром.
Статья необъективная. В примере с thunk'ом сравниваются 2 разные функции. Так более справедливо:

const getUsers = () => 
  async (dispatch) => {
    try {
      const users = await APIService.get('/users')
      dispatch(successGetUsers(users))
    } catch(err) {
      dispatch(failedGetUsers(err))
    }
  }

и тоже самое на TypeScript:

// 1 раз на всё приложение:
export type AppThunk = ThunkAction<void, {}, {}, AnyAction>

const getUsers = (): AppThunk => 
  async (dispatch) => {
    try {
      const users = await APIService.get('/users')
      dispatch(successGetUsers(users))
    } catch(err) {
      dispatch(failedGetUsers(err))
    }
  }

Ещё не надо указывать тип для переменной users — TypeScript автоматически выведет тип на основе результата метода get. Продвинутый вывод типов и отличает TS от Java, где нужно писать BeanFactory beanFactory = new BeanFactory(). Второй пример c Express-сервером прекрасно работает с TS, не нужен там класс.

Господа минусующие, потрудитесь объяснить в чём я не прав. Ставить минусы исподтишка много ума не надо.

Идея в том, что селектор должен пересчитываться только тогда, когда поменяются переменные, на основе которых он считается — products и totalCount. В вашем примере это не соблюдается. Предлагаю более наглядный пример из документации Redux c зависимыми селекторами:

const getVisibilityFilter = state => state.visibilityFilter;
const getTodos = state => state.todos;

export const getVisibleTodos = createSelector(
  [getVisibilityFilter, getTodos],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed);
    }
  }
);

const getKeyword = state => state.keyword;

const getVisibleTodosFilteredByKeyword = createSelector(
  [getVisibleTodos, getKeyword],
  (visibleTodos, keyword) =>
    visibleTodos.filter(todo => todo.text.includes(keyword))
);

На Mobx это будет выглядеть так:

@computed get visibleTodos() {
  switch (this.visibilityFilter) {
    case 'SHOW_ALL':
      return this.todos;
    case 'SHOW_COMPLETED':
      return this.todos.filter(t => t.completed);
    case 'SHOW_ACTIVE':
      return this.todos.filter(t => !t.completed);
  }
}

@computed get visibleTodosFilteredByKeyword() {
  return this.visibleTodos.filter(todo => todo.text.includes(this.keyword));
}

Мало того, что пример на Mobx объективно меньше и проще, так он ещё удобнее для рефакторинга — зависимости селектора не нужно прописывать вручную, а значит если нужно добавить ещё одну переменную на основе которой вычисляется значение, то мы добавим её в одном месте, а не в двух.
Despite using useObserver hook in the middle of the component, it will re-render a whole component on change of the observable.

Классовый компонент с декоратором observer будет работать так же — компонент будет полностью перерисовываться когда хотя бы одно из observable значений в render методе поменяется. Если хотим меньше перерисовок — либо разбиваем компонент на компоненты поменьше (тогда перерисовываться будут только дочерние), либо используем <Observer />. На практике мне всегда хватало первого варианта.

Декоратор observer всё ещё нужен.

Я не точно выразился. Имел в виду, что не обязательно прописывать experimentalDecorators в tsconfig, Mobx может использоваться и без декораторов:

export const Counter = observer(() => {
  return (
    <div>
      <span>{counter.count}</span>
      <button onClick={counter.inc}>Increment</button>
    </div>
  )
})

Работает так же, как и старый пример с декоратором:

@observer
export class Counter {
  render() {
    <div>
      <span>{counter.count}</span>
      <button onClick={counter.inc}>Increment</button>
    </div>
  }  
}

Хуки же позволяют полностью переключиться с классовых компонентов на функциональные. Уже давно перестал использовать декораторы observer и inject, проблем в продакшене не было.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность