All streams
Search
Write a publication
Pull to refresh
29
0
Егор Горбачёв @kubk

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

Send message
Всё-таки 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, проблем в продакшене не было.
Ментейнеры Redux определённо проделали хорошую работу над ошибками. Всегда причислял к проблемам Редакса нечитабельное обновление вложенных объектов и сложности с типизацией. Наконец-то это дошло и до ментейнеров. Однако в Redux есть проблемы, не решаемые by design:

1) Необходимость держать стейт в нормализованном виде из-за иммутабельности. Иммутабельное обновление данных подразумевает, что все вложенные объекты тоже должны быть скопированы, что будет триггерить перерисовку компонентов, данные в которых остались по факту теми же. Об этом написано в документации. Проблема решается нормализацией данных, что только добавляет головной боли — придётся нормализовывать данные с бекенда перед вставкой в стор и денормализовывать обратно перед отправкой на сервер. Получается ORM на фронте, с Mobx это не нужно.

2) Для мемоизации нужно писать селекторы с ручным указанием зависимостей. Допустим у вас есть страница с товарами и кнопка «Load more», которая запрашивает товары с сервера пачками по N штук. Кнопка должна пропасть, если страниц с товарами больше нет. Это произойдёт когда количество загруженных товаров на странице станет равно общему количеству товаров на бекенде.

Код на reselect будет выглядеть так:

export const getProductPage = (state: RootState) => state.productPage;

export const isLastPage = createSelector(
  createSelector(getProductPage, page => page.products),
  createSelector(getProductPage, page => page.totalCount),
  (products, totalCount) => products.length === totalCount
);

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

@computed get isLastPage() {
  return this.products.length === this.totalCount;
}

Не нравятся декораторы — используйте функции. Декораторы inject и observer больше не нужны с выходом хуков. Mobx будет автоматически пересчитывать значение isLastPage когда хотя бы одна из зависимостей (products или totalCount) изменится.

Вычисления в селекторах на практике оказываются более сложными — например таблица с множественными аггрегациями по определённым полям и строкам. В таких случаях пересчитывания на каждый рендер компонента могут ухудшить UX. Для этих целей придумали мемоизацию и в Redux это делается очень многословно.
Здорово, что пишете о необходимости строгих проверок, но тема строгости не раскрыта. Мало выставить строгие проверки, нужно ещё код писать по-другому — где-то дополнительно описывать типы, где-то добавить проверки для сужения типов. К примеру, с выключенным «strictNullChecks» такой код успешно скомпилируется:

const foo: number = null;

Тип объявлён как number, а присвоить туда можно null. Такой код — источник известной проблемы Null Pointer Exception. Тут нужно либо сменить тип на number | null либо проанализировать код и задаться вопросом — а можно ли здесь вообще обойтись без null? Если foo сразу инициализовать числом, то null окажется ненужным. Необходимость писать код по-другому касается и других строгих опций, в особенности «strictPropertyInitialization» и «noImplicitAny».

Для того, чтобы TypeScript был более строгим не только TS-файлах, но в Angular-шаблонах нужно включать опцию 'fullTemplateTypeCheck' в tsconfig.json:

{
    "compilerOptions": { ... },
    "angularCompilerOptions": {
        "fullTemplateTypeCheck": true
        ...
    }
}

Половина опций в вашем tsconfig.json лишняя, так как strict: true уже включает в себя «noImplicitAny», «noImplicitThis», «alwaysStrict», «strictBindCallApply», «strictNullChecks», «strictFunctionTypes» и «strictPropertyInitialization»: www.typescriptlang.org/docs/handbook/compiler-options.html

Так и не понял зачем в статью о строгом Ангуляре включать настройку гита и баш-скрипты для NVM под макось.

Information

Rating
Does not participate
Registered
Activity