Pull to refresh

Comments 62

Effector — очень крутой стейт-менеджер.

Вы в слове «крутой» сделали 8 ошибок, ведь правильно писать «ущербный».

К чему вы всё это написали то, мы же вроде в нашей эре живем? Тем более в 2021 году, MobX уже давным давно все это заменил.

Никаких спорных вещей вроде Proxy или декораторов.

Спорных???
1) Декораторы хотите используйте, хотите не используйте, это опционально.
2) Proxy уже 100 лет в обед существует в языке и шикарно себя показывает.

Где тут спорность то? Что-то ломается, что-то не работает?
Хах, тут для автора Proxy это спорная вещь, а писать пять функций, чтобы получить поле из запроса это ни разу не спорно)

Не нужно писать 5 функций, можно писать по 1 селектру для каждого нужного снаружи поля и дублировать пути в разных селекторах. Так будет проще читать так как сразу видно весь путь и нет кучи вложенных функций

Думаю автор хотел показать саму суть того, что селекторы можно вкладывать и переиспользовать код, но злоупотреблять этим не стоит

По опыту на разных проектах могу сказать, что глубоко вложенное поле может пройти путь через несколько селекторов, при этом они его будут модифицировать и в итоге задача на 5 минут легко растянется на час, учитывая, что дебажить селекторы ещё то удовольствие. И наличие такого кода есть минус самой архитектуры, которая позволяет такое накрутить.

Селекторы - чистые функции, не понимаю какие проблемы с дебагом тут могут быть. Чистые функции самое пригодное для дебага что только может быть.

Проблемы с вложенностью можно получить используя что угодно, даже тот же MobX если навкладывать вычисляемые свойства один в другой несколько раз. Просто нужно контролировать вложеннсть абстракций. В случае с чистыми функциями это супер просто так как часто можно заменить вкладывание на функциональную композицию. В случае с классами, методами и остальными более сложными абстракциями дела обстоят куда сложнее.

Если получится большая вложенность вероятность велика что декомпозиция была выполнена не оптимально

Вот действительно, читал-читал статью, а потом такое.


Декораторы — это вообще синтаксический сахар, а не функционал.
А Proxy..., ну еще один инструмент языка.


Видимо, это из той же серии, что с "Мы, в Реакт, переходим на фунциональные компоненты, потому что классы — это слишком сложно".


In addition to making code reuse and code organization more difficult, we’ve found that classes can be a large barrier to learning React. You have to understand how this works in JavaScript, which is very different from how it works in most languages.

Спорных???

1) Декораторы хотите используйте, хотите не используйте, это опционально.

Тут, на самом деле, всё ещё более просто. Код пишется на TS, а значит, неважно, какой там статус у JS-ных декораторов - мы мыслим в рамках конструкций TS. Худшее, что может быть - декораторы в JS утвердятся, но слегка с другим синтаксисом. Тогда по идее TS должен автоматом поддержать старый вариант, для обеспечения совместимости.

тем временем в TS
NOTE  Decorators are an experimental feature that may change in future releases.
тем временем в TS
NOTE  Decorators are an experimental feature that may change in future releases.

И что? Я их как в 2015 году писал, точно так же и пишу в 2021, 6 лет ими пользуюсь, одни плюсы, ни одного минуса. Обратная совместимость как минимум останется на долгие годы, так что минимум в ближайшие лет 10 можно вообще не думать по этому поводу.
а чем гарантирована обратная совместимость эксперементальной фичи?
Завтра выходит какой-нибудь ts 5 и ее выпиливают.
Если фича 6 лет не может из эксперементальной перейти в стабильную, то это о чем то да говорит

Эмм, ну и пусть выходит, у вас что в package.json пакеты через * указаны?

Можете раскрыть свою мысль про ущербность effector?

Можете раскрыть свою мысль про ущербность effector?

Далеко ходить не нужно даже за примерами из реального кода, открываете главную страницу эффектора и видите эту дичь:
import {createEvent, createStore, createEffect, sample} from 'effector'

const nextPost = createEvent()

const getCommentsFx = createEffect(async postId => {
  const url = `posts/${postId}/comments`
  const base = 'https://jsonplaceholder.typicode.com'
  const req = await fetch(`${base}/${url}`)
  return req.json()
})

const $postComments = createStore([])
  .on(getCommentsFx.doneData, (_, posts) => posts)

const $currentPost = createStore(1)
  .on(getCommentsFx.done, (_, {params: postId}) => postId)

const $status = combine(
  $currentPost, $postComments, getCommentsFx.pending,
  (postId, comments, isLoading) => isLoading
    ? 'Loading post...'
    : `Post ${postId} has ${comments.length} comments`
)

sample({
  source: $currentPost,
  clock: nextPost,
  fn: postId => postId + 1,
  target: getCommentsFx,
})

$status.watch(status => {
  console.log(status)
})

nextPost()


Вот повторение этого примера с использованием MobX'a:
import { autorun, observable } from "mobx";

class Posts {
    @observable isFetching = false;
    @observable.ref posts = [];
    postId = null;

    fetchPosts = async (postId) => {
        this.postId = postId;
        this.isFetching = true;
        const req = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`);
        this.posts = await req.json();
        this.isFetching = false;
    };
}

const postsState = new Posts();

(async () => {
    await postsState.fetchPosts(1);
    await postsState.fetchPosts(22);
})();

autorun(() => {
    if (postsState.isFetching) {
        console.log("Loading post...");
    } else {
        console.log(`Post ${postsState.postId} has ${postsState.posts.length} comments`);
    }
});


Очевидно что в effector'e лапша и надо напрягаться чтобы понять что там происходит, когда используешь MobX, то всё элементарно, просто смотришь сверху вниз, слева направо и все сразу понятно.

Effector использует топорный pub/sub, так ещё и в добавок ко всему иммутабильный, со всеми вытекающими проблемами и неудобствами, хотя JS уже более 10ти лет дает мощную штуку в виде getters/setters и последние 6 лет дает ещё более продвинутую штуку в виде Proxy.

Поэтому он и ущербный, раскрывает возможности языка на 0 из 10, добавляет удобство разработчику на 0 из 10.
import {createStore, createEffect} from 'effector'

const fetchPostsFx = createEffect(async (postId) => {
  const req = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`);
  return req.json();
})

const $posts = createStore([]).on(
  fetchPostsFx.doneData,
  (_, posts) => posts,
)

await fetchPostsFx(1)

в случае с effector мне и флаги переключать не надо, и ошибки обрабатывать, потому что createEffect об этом уже позаботился
Тут вопрос даже не в количестве кода, а в том, насколько понятно, что там вообще происходит. У Mobx нет никакой философии, это просто JS класс, который обычно используется как Singleton. То есть, если убрать из этого класса observable, action и computed, то в коде смогут разобраться даже далёкие от js люди. И всё это добро идёт вместе с космической оптимизацией, которую ни один immutable стейт менеджер априори не сможет дать без лишних телодвижений. Effector же как и Redux пытается как и редакс привнести какую-то свою идеологию или чуждый АПИ, который, скорее всего, понадобится только при работе с этим конкретным менеджером. Вот и возникает вопрос: зачем делать плохо, если можно сделать хорошо?
чуждый апи людям не знающим redux. про космические оптимизации вы конечно же загнули. У effector'а довольно неплохо с производительностью. В mobx же довольно много спорного синтаксиса, например: декораторы, runInAction и из последнего makeAutoObservable(дикий костыль как по мне). классы — это чуждый апи для js в принципе, любой человек писавший на языках типа java, удивится пляске вокруг this
декораторы можно не использовать, это дело вкуса. Вообще не понимаю, чего все вы так к декораторам прицепились, можно и без них писать. В любом случае в большинстве проетов стоят свои настройки сборщика и именно у вас на проекте все будет работать как и задумывалось изначально. А runInAction это алиас для action, в чём его спорность?
чуждый апи людям не знающим redux

Конечно, можно разобраться в архитектуре flux. Можно все время держать в голове, что у вас иммутабельность, и нельзя мутировать данные, можно делать flat store, потому что архитектура плохо работает с вложенными данными, можно писать много лишнего кода на пустом месте(селекторы) и тд и тп. А можно просто использовать инструменты, которые не налагают своих ограничений и позволяют сконцентрироваться на бизнес-логике это дело лично каждого.
про космические оптимизации вы конечно же загнули

Если в моем сторе есть данные вида User.details.personal.firstName и есть какой-то компонент который подписан только на firstName, он будет перерендериваться ТОЛЬКО если поменяется firstName. Effector так может из коробки?
Если в моем сторе есть данные вида User.details.personal.firstName и есть какой-то компонент который подписан только на firstName, он будет перерендериваться ТОЛЬКО если поменяется firstName. Effector так может из коробки?

офк может
Конечно, можно разобраться в архитектуре flux. Можно все время держать в голове, что у вас иммутабельность, и нельзя мутировать данные, можно делать flat store, потому что архитектура плохо работает с вложенными данными, можно писать много лишнего кода на пустом месте(селекторы) и тд и тп.

effector избавлен от этих болезней
декораторы можно не использовать, это дело вкуса.

и там появляется еще более уродский синтаксис, синтаксис с makeAutoObservable

классы — это чуждый апи для js в принципе

сейчас стандартом становится TS, а там классы ну совсем не чуждые. Проблема с this давно уже не проблема - оформляем метод как стрелочную функцию, и всё.

Проблема с this давно уже не проблема — оформляем метод как стрелочную функцию, и всё.
Легаси никто не отменял. И я встречал людей намеренно не использующих стрелочные в классах.

Некоторые не без причин намеренно не используют стрелочные функции в классах. Одна из известных мне проблем - если класс наследуется от другого и в базовом классе есть метод, оформленный как стрелочная функция и использующая this, то этот метод не будет работать с наследником.

То есть, если убрать из этого класса observable, action и computed, то в коде смогут разобраться даже далёкие от js люди


поправьте если не прав, но ведь тогда это будет не mobx?
По такой логике, просто уберите из redux actions/reducer/store и в нем сможет разобраться каждый

Эт я к тому, что всё, что делает мобх это "добавляет реактивности" нативному js классу. На мой взгляд, такой подход интуитивно более понятен, чем разучивать новые парадигмы в которых живëт тот или иной стейт менеджер.

import { autorun, observable } from "mobx";

class WithFetch {
    @observable isFetching = false;
    @observable errorMessage = null;
    @observable.ref data = null;

    get = async (url: string) => {
        this.isFetching = true;
        try {
            const req = await fetch(url);
            const data = await req.json();
            this.data = data;

            return data;
        } catch (e) {
            console.error(e);
            this.errorMessage = e.message;
            throw new Error(e);
        } finally {
            this.isFetching = false;
        }
    };
}

class Posts {
    fetcher = new WithFetch();
    postId = null;

    fetchPosts = async (postId) => {
        this.postId = postId;
        await this.fetcher.get(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`);
    };
}

const postsState = new Posts();

(async () => {
    await postsState.fetchPosts(1);
    await postsState.fetchPosts(22);
})();

autorun(() => {
    if (postsState.fetcher.isFetching) {
        console.log("Loading post...");
    } else {
        console.log(`Post ${postsState.postId} has ${postsState.fetcher.data.length} comments`);
    }
});


Ну вот, пожалуйста, не переключайте флаги, делов то, это ж элементарно:
class WithFetch {
    @observable isFetching = false;
    @observable errorMessage = null;
    @observable.ref data = null;

    get = async (url: string) => {
        this.isFetching = true;
        try {
            const req = await fetch(url);
            const data = await req.json();
            this.data = data;

            return data;
        } catch (e) {
            console.error(e);
            this.errorMessage = e.message;
            throw new Error(e);
        } finally {
            this.isFetching = false;
        }
    };
}

Один раз написал обертку для запросов к серверу и усё, получаешь удовольствие.
эм, ну это уродство, и помоему вы забыли про runInAction. и кстати кода уже в разы больше стало
эм, ну это уродство, и помоему вы забыли про runInAction. и кстати кода уже в разы больше стало

1) Уродство — это ваше представление о прекрасном. Нужно писать код максимально просто и понятно, и никак иначе.

2) runInAction конкретно тут не нужен, как и action. Вы же вообще не понимаете зачем они нужны и когда их нужно и использовать, однако пытаетесь вставить свои 5 копеек, более того вы говорите makeAutoObservable — уродливый синтаксис, в каком месте он уродливый то?) makeAutoObservable(this) в конструкторе класса, одна строчка, где тут уродство то?) Че за дичь вообще вы несете то?)

3) Кол-во кода в разы больше? Мда, а в в курсе что классы/функции и т.п. можно выносить в разные файлы и импортировать оттуда?) Вам привели наглядный пример где все специально в одном файле написано.
2) runInAction конкретно тут не нужен, как и action. Вы же вообще не понимаете зачем они нужны и когда их нужно и использовать, однако пытаетесь вставить свои 5 копеек, более того вы говорите makeAutoObservable — уродливый синтаксис, в каком месте он уродливый то?) makeAutoObservable(this) в конструкторе класса, одна строчка, где тут уродство то?) Че за дичь вообще вы несете то?)

Я вроде сказал «вроде». makeAutoObservable(this) — это костыль, я ошибся когда сказал, что это уродство. Вам нормально когда у вас в конструкторе непонятные сайд эффекты? Одна строчка где тут костыль-то? че за дичь я вообще несу-то?).
3) Кол-во кода в разы больше? Мда, а в в курсе что классы/функции и т.п. можно выносить в разные файлы и импортировать оттуда?) Вам привели наглядный пример где все специально в одном файле написано.

Лапша? мда, я думал в эффекторе тоже можно вносить функции в разные файлы и импортировать оттуда?). Вам привели наглядный пример где все специально в одном файле написано. И вы в коде опустили часть с комментами и сократили код с fetch'ем в одну. Так и я могу говорить, что лапши нет.
rickets а вы похоже очень «умный» и «рассудительный» человек, сразу видно разбираетесь в теме, продолжайте в том же духе.
Не настолько «умен» и «рассудителен» как вы
когда используешь MobX, то всё элементарно, просто смотришь сверху вниз, слева направо и все сразу понятно.

few moments later…
runInAction конкретно тут не нужен, как и action. Вы же вообще не понимаете зачем они нужны и когда их нужно и использовать


так мобыкс простой и без философии или таки нет?
так мобыкс простой и без философии или таки нет?

Простой, action/runInAction это грубо говоря транзакция. Их есть смысл использовать только когда сразу несколько реактивных переменных меняешь. В остальных случаях без разницы, хочешь заворачивай, хочешь нет.

import { autorun, observable, runInAction } from "mobx";

const counter = observable({ count: 1 });

autorun(() => {
  console.log("count is:", counter.count); 
});

counter.count++; // count is: 2
counter.count++; // count is: 3
counter.count++; // count is: 4

runInAction(() => {
  counter.count++;
  counter.count++;
  counter.count++;
});

// count is: 7


Вот и вся разница. codesandbox.io/s/sharp-tereshkova-ysbsq?file=/src/index.js
Простой, action/runInAction это грубо говоря транзакция.

транзакция или батчинг?
Че за дичь вообще вы несете то?)

Селекторы - самая тухлая часть Редукса (точнее, подхода single-store). Я сейчас даже не про танцы с бубном вокруг (ре-)реселекта. В каждый селектор по сути передается ВЕСЬ стор. Ну то есть бардак в зависимостях в полный рост. Особенно доставляет тот факт, что имеющаяся в приложении декомпозиция стора, сотворенная через combineReducers (и получившая развитие в "слайсах" тулкита), должна быть повторена ещё и в селекторах.

Сравните это с атомарным подходом, особенно МобХ в стиле ООП, где класс существует "сферично в вакууме", явно получает на вход свои зависимости в виде интерфейсов, и содержит в себе как экшены, так и селекторы, то есть всё необходимое, инкапсулируя данные.

ООП с DI на MobX - это не то, чтобы эффективный подход... Он лучше любого на Redux, но в целом недостатков много - классовый DI делает хранилища запутанными и несемантичными, привносит дополнительный бойлерплейт, становится сложно контролировать циклические зависимости. Экшены часто 1) семантически не подходят стору; 2) используют параметры из нескольких сторов; 3) меняют параметры в нескольких сторах. Первый и второй пункты подходят и для селекторов.

В общем случае есть намного более удобные решения с глобальным расширяемым хранилищем и отдельными от сторов экшенами и селекторами. Тестирование через моки файлов и контекста в целом не сложнее настроить, чем пробрасывать фейковые зависимости в DI.

классовый DI делает хранилища запутанными и несемантичными, привносит дополнительный бойлерплейт, становится сложно контролировать циклические зависимости

Бойлерплейта нет только если в коде полная анархия (мать порядка). Так что его просто надо минимизировать. В классовом DI его немного.

Циклические зависимости чувствуют себя неуютно, если DI через конструктор. В любом случае, зависимости заданы более явно, чем в селекторах (но это субъективно).

В общем случае есть намного более удобные решения с глобальным расширяемым хранилищем и отдельными от сторов экшенами и селекторами. Тестирование через моки файлов и контекста в целом не сложнее настроить, чем пробрасывать фейковые зависимости в DI.

Дело не только в тестировании. Как только у некоторого модуля появляются селекторы и зависимость от глобального стора, модуль прибивается гвоздями к этому стору и получает в нем некое фиксированное место. Наверно, не стоит говорить о минусах такой ситуации. Классы с DI, а так же редуксовые редьюсеры/экшены/слайсы таким недостатком не страдают - они прикрепляются уже при использовании.

Все аргументы в пользу ущербного DI высосаны из пальца и не относятся к реальной жизни.

Самый абсурдный «аргумент» конечно:
Как только у некоторого модуля появляются селекторы и зависимость от глобального стора, модуль прибивается гвоздями к этому стору и получает в нем некое фиксированное место.

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

Вы может в вашей машине двигатель и КПП наружу сразу выкидываете, чтобы можно было их быстро подменить на табуретку и журнальный столик, правда машина ездить не будет, но зато вы же возможность оставили, и не важно насколько это убого выглядит и сколько приносит неудобств, ведь главное чтобы была ненужная возможность на подмену.

Вы может в вашей машине двигатель и КПП наружу сразу выкидываете, чтобы можно было их быстро подменить на табуретку и журнальный столик, правда машина ездить не будет

Тут соглашусь, на табуретке далеко не уедешь..

Однако вот у меня в системном блоке есть разъем для hdmi. И я могу подключить любой монитор, который реализует интерфейс hdmi. Как вам такое сравнение? Почему я не купил себе комп, завязанный на некий "один всемирный монитор"? ))

В классовом DI бойлерплейта много по сравнению с отдельными экшенами и селекторами. Про явность зависимостей тоже некорректно, сравните:


class StoreRouting {
  storeUser?: StoreUser;
  pageName = 'Profile';

  constructor(storeUser: StoreUser) { this.storeUser = storUser; }

  pageTitle() {
    return this.pageName + ' ' + this.storeUser.userName;
  }
}
// и при создании инстанса надо не забыть о пробросе storeUser и порядке создания констант, при этом ряд инджектов не будет доступен

***

const getters = {
  pageTitle() {
    return globalStore.routing.pageName + ' ' + globalStore.user.userName;
  }
}

Как я и писал выше — классы не теряют семантичность, не спутываются сложными взаимосвязями (не раз видел DI полудюжины сторов и десятки толстых функций-геттеров, никак не относящихся по сути к данному стору), а описание отдельных геттеров требует намного меньше бойлерплейта и зависимости там более явные. То же самое относится и к организации экшенов-модификаторов.


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

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

Чем плох коннект к глобальному объекту? Чем вообще плохи глобальные переменные? Вроде бы, об этом говорилось уже много раз. Код становится более жестким, не получится использовать модуль в другой ситуации, и прочая и прочая. К тому же это нарушает принципы SOLID, например, тот же SRP (да, внедрение зависимостей как раз об этом) - модуль, кроме выполнения своей задачи, самостоятельно резолвит свои зависимости, ну и конечно, DIP - имеем довольно злостную зависимость от реализации. Если не работать с глобальной переменной, а передавать стор в конструктор/функцию как ссылку на интерфейс, то предыдущие две проблемы лечатся, зато нарушаем ISP, так как имеем один здоровенный интерфейс, из которого нам нужна только малая часть.

Относиться к SOLID можно по-разному, можно вполне себе без него жить, но с ним, по моему, легче. Хотя повторюсь, всё это субъективно. Навязывать мнение не собираюсь, просто изложил тут свои взгляды на жизнь )

Относиться к SOLID можно по-разному, можно вполне себе без него жить, но с ним, по моему, легче.

SOLID усложняет всё очень сильно, при чем на ровном месте. Любая элементарщина становится очень громоздкой и сложной. Как минимум его использование на фронте тотально не оправдано, от слова совсем и несет только серьезный ущерб проекту и колосальный ущерб времени.
Чем плох коннект к глобальному объекту?

Ничем, от слова совсем. Реально, без шуток. Потому что ему нужен глобальный стор, без него он бесполезный, вот прям совсем бесполезный.
Чем вообще плохи глобальные переменные?

Ничем, от слова совсем. Реально, без шуток.
Код становится более жестким, не получится использовать модуль в другой ситуации, и прочая и прочая.

Кто сказал что вы каждый компонент/модуль будете переиспользовать где-то, когда-то потом, но прямо сейчас этого точно не надо? Вы тратите 100 условных единиц времени сверху, на ваш гребаный SOLID и прочую чушь в стиле, а вдруг он будет переиспользоваться где-то и т.п., но по факту экономите 2-3 условные единицы времени в реальной жизни, потому то ваши притянутые за уши ситуации настолько редко встречаются и настолько просто решаются, что ими вообще можно принебречь и в сухом остатке:
— Вы тупо прожигаете время в пустую.
— Пиши очень много лишнего кода.
— Делаете код очень запутанным.
— Повышаете bus фактор на проекте до максимальных величин.
— Фикс элементарных багов теперь съедает сильно много времени.
— Добавление изменение фичи теперь съедает сильно много времени.

И зачем всё это? Ради чего? Вот реально все только хуже хуже и хуже становится, сами себе веревку на шее завязываете, более того остальным мешаете навязываю свою «архитектуру», а потом меняете место работы и вуаля, образуется сразу же труп который переписывается с нуля.

"неплохо бы думать как о потенциально переиспользуемом" !== "внятный минималистичный API"


Возьмем такой пример, в продолжение предыдущего поста:


render() {
  const { store } = this.context;

  return (
    <>
     <Component1 pageTitle={store.routing.pageTitle} />
     <Component2 pageTitle={store.routing.pageTitle} />
    </>
  );
}

В этом кейсе:


  • родительский компонент обновляется, когда не должен из-за изменения pageTitle
  • в дочерних компонентах нужно указывать propTypes
  • распространен кейс, когда этим дочерним компонентам не нужен pageTitle, а он нужен еще глубже. Предыдущие проблемы умножаются, приложение тормозит, длинные цепочки props drilling…
    А когда такой параметр не один, а много? Очевидно, что увеличивается и связка компонентов, и осложняется рефакторинг (вполне можно "забыть" убрать у одного из родителей этот параметр), и код становится замусоренным.

С глобальным компонент любой вложенности может брать store.routing.pageTitle из контекста. Мокается не сложнее, чем <StoreContext.Provider value={{ routing: { pageTitle: 'sample' }}}>. Обновляются компоненты точечно, пропсы не раздуваются, рефакторинг одной строчкой. Какие бы принципы ООП это ни нарушало, простота и поддерживаемость — то, для чего все принципы программирования создаются, и если в React+MobX какие-то неэффективны, не следует вслепую их применять.


Компонент, который подключается к глобальному стейту не "самостоятельно резолвит свои зависимости", а использует переданные, но только не через раздутые propTypes, а через контекст. Нет ничего зазорного, чтобы передавать данные через этот интерфейс — пусть он не настолько явный, но очень удобный для реактивных хранилищ.


По поводу классового DI — это тоже применение паттернов, которые не оправданы реальным использованием. Если классы-хранилища не будут знать друг о друге и изолированы, но из двух классов можно сделать выборку данных и реактивно эту выборку (селектор) обновлять — сколько тут принципов нарушается? Кажется, меньше, чем с DI. Как понимаю, основной консерн у вас в доставшемся от Редакса концепте "надо давать в компоненты как можно меньше данных, а не стор целиком" — это травма, вызванная иммутабельностью и неточечной реактивностью, уже можно забыть об этом. Если мы дадим в компонент весь стор, а он использует из него один или сто параметров — с MobX не важно, но это максимально удобно.

родительский компонент обновляется, когда не должен из-за изменения pageTitle, в дочерних компонентах нужно указывать propTypes

Тут уже вопрос, чем мы готовы пожертвовать - перерендером или гибкостью (чисто реактовская специфика). Конкретно в данном примере подойдет MobX-way:

interface Component1Props {
  routing: IRoutingStore
}

Тут нет лишнего перерендера, но всё ещё не лезем в общий контекст внутри Component1. Можно ещё чутка упороться в ISP и передать не IRoutingStore, а какой-нибудь ITitleOwner { pageTitle: string }

Мокается не сложнее, чем <StoreContext.Provider value={{ routing: { pageTitle: 'sample' }}}>.

Увы, увы... В Typescript этот StoreContext потребует себе весь стор. Можно конечно схитрить и вручную сделать приведение типа, но это неспортивно )

На самом деле я тоже сторонник хранить сторы в контексте. Но в компонентах они должны быть доступны через отдельные хуки, которые их из контекста вытаскивают. Тогда опять же не придется в компонентах обращаться к глобальному объекту или даже контексту, и вообще компонент не будет знать, откуда взялся конкретный стор или модуль.

По поводу классового DI — это тоже применение паттернов, которые не оправданы реальным использованием. Если классы-хранилища не будут знать друг о друге и изолированы, но из двух классов можно сделать выборку данных и реактивно эту выборку (селектор) обновлять — сколько тут принципов нарушается? Кажется, меньше, чем с DI.

Не очень понял идею, можно пример?

Как понимаю, основной консерн у вас в доставшемся от Редакса концепте "надо давать в компоненты как можно меньше данных, а не стор целиком" — это травма, вызванная иммутабельностью и неточечной реактивностью

Нет, пожелание про "меньше данных в компоненты" - всё тот же вышеупомянутый ISP. Чем меньше зависимостей, тем слабее зацепление, менее жесткий код, то есть больше "запас прочности" в проекте

"Тут нет лишнего перерендера" факт, но для чего лишний проп? Если для виртуального рендеринга компонента в тестах, то подойдет и "неспортивный" мок контекста. В остальных случаях в этот компонент не будет передаваться другой router, кроме как глобальный, поэтому можно упростить интерфейс.


Я много раз пробовал схему подключения конкретных сторов через useContext или inject в декораторах (если классовый подход), но это исключительно лишний код — не было ни одного случая, когда это принесло бы пользу, кроме спорного "явного указания зависимостей". Так что почему "должны"? Может, есть специфический кейс, о котором я не знаю? В чем "слабое зацепление" в виде передачи многих одинаковых пропсов в компоненты или многочисленных useContext дает больше "запаса прочности"?


Пример селекторов, независимых от стора, привел в const getters.

Селекторы это что-то типа view в базах данных. В общем случае они могут использовать данные из произвольных частей стора. Поэтому их иерархия не обязана повторять структуру редьюсеров (которые больше про атомарность обновления тесно связанных данных).

Просто селектор будет вызываться при каждом рендере, а не только когда обновились данные в сторе.
Вот эта логика в исходниках useSelector.

Вообще это логика элементарно переопределяется вторым параметром.

Во-первых, переопределяется другое, а именно - сравнение результата селектора с предыдущим. То есть, селектор все равно выполнится. Переопределение компаратора в большинстве случаев ничего не именит, если там не возвращается каждый раз новый объект, а вы не передали туда shallowEqual. Вообще выглядит так, будто этот комментарий должен цитировать совсем другую часть текста (там, где было про сравнение через ===.

Ну и во-вторых, второй параметр useSelector - костыль:

  • Вместо 1 места (в селекторе) надо во всех местах его использования указывать функцию-компаратор - это все усложняет и ведет к ошибкам

  • Как указал @Carduelisв этом комменте - банально все может сломаться на более вложенной структуре

  • Хуже производительность

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

Вместо 1 места (в селекторе) надо во всех местах его использования указывать функцию-компаратор — это все усложняет и ведет к ошибкам

Это же просто вопрос договоренности в проекте. Вам ничто не мешает просто определить в корне проекта useShallowSelector и забанить useSelector линтером. В одном проекте мы так делали, просто потому что по результатам голосования в чате телеграма такой подход признали более удобным большинством. А reselect использовать только когда shallowEqual не работает. Ну то есть мы «инкапсулируем мемоизацию в другом месте», просто делать это приходится вдвое-втрое реже.

Ну и у useSelector не второй параметр — костыль, а весь вызов. Из-за того, что он не передает ownProps компонента в селектор, его все равно часто приходится вызывать как
    const selector = useMemo(
        () => makeGetSomeListRowSelector(groupName),
        [groupName]
    );
    const { list, isLoaded, isLoading, selected } = useSelector(selector);


И вот с этим уже бороться сложнее. Я собственно знаю только два подхода.
— Не использовать селекторы сложнее, чем просто забрать по адресу из стора (возможно используя при этом что-то из ownProps). Соответственно все вычисления перелазят в слайсы-редьюсеры (это кстати и отлаживать проще). Но поскольку это не MobX / Effector и даже не $mol следить за пересчётом приходится самому.
— Не использовать Redux, раз они за 5 лет не смогли придумать нормальных computed полей. Особенно если саги не нужны. В идеале, конечно Apollo, но его использование для меня было сопряжено с тем, что мне и кусок бэка приходилось писать. Как дефолтный не плох MobX, но чувствую, что народ сейчас начнет перелазить на Effector.
А reselect использовать только когда shallowEqual не работает. Ну то есть мы «инкапсулируем мемоизацию в другом месте», просто делать это приходится вдвое-втрое реже.


Это уже ведет к:
  • неконсистентности (совершенно разным подходам в разных местах)
  • невозможности использовать как входной параметр в мемоизированном селекторе (проверка аргументов всегда будет фейлиться)


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

Ну и у useSelector не второй параметр — костыль, а весь вызов. Из-за того, что он не передает ownProps компонента в селектор


Согласен — было бы намного удобнее. Я бы вообще убрал параметр с кастомным компаратором, и вместо этого позволил пробрасывать произвольное число аргументов в селектор.

Не использовать селекторы сложнее, чем просто забрать по адресу из стора (возможно используя при этом что-то из ownProps). Соответственно все вычисления перелазят в слайсы-редьюсеры (это кстати и отлаживать проще). Но поскольку это не MobX / Effector и даже не $mol следить за пересчётом приходится самому.


Производные от исходных данных в редьюсере — катастрофа на большом проекте. У меня однажды в одной фиче было где-то 40, если не больше, селекторов, между которыми были довольно сложные зависимости, с точки зрения всей картины. Полагаю, в редьюсер это было бы невозможно засунуть и проект бы несчадно лагал при любом экшне.

Селекторы и в частности `reselect` как раз позволяют строить зависимости между различными данными и достаточно эффективно производить пересчеты. И особенно хорошо, когда абсолютно все получение данных и мемоизация разруливаются там же (проецируя на предложение с `shallowEqual`).

Не использовать Redux, раз они за 5 лет не смогли придумать нормальных computed полей. Особенно если саги не нужны.


Да, это один из посылов статьи)

И еще добавлю про саги — это как раз еще один костыль для редаксом, который дает ему возможность контролировать flow.

Более-менее жизнеспособное использование редакса строится именно из таких костылей. Саги — для control flow, селекторы — как замена всяким компьютедам, тулкит — как абстракция для некоторых продвинутых возможностей и фиксинга бойлерплейта.

Сам по себе редакс — просто очень примитивная технология без всего этого.
Всё так. Ну зато расширяется хорошо.

Вот только что меня в очередной задолбало писать приведенный выше пример, и я дописал в проект обертку useSelectorCreator.

В общем главная моя претензия к Redux — он стал дефолтным менеджером состояний. Будь у него популярность на уровне RxJS или даже MobX, я бы не имел к нему никаких претензий. А вот тот же MobX разумнее было бы выбирать именно дефолтным.
И еще добавлю про саги — это как раз еще один костыль для редаксом, который дает ему возможность контролировать flow.

Более-менее жизнеспособное использование редакса строится именно из таких костылей. Саги — для control flow, селекторы — как замена всяким компьютедам, тулкит — как абстракция для некоторых продвинутых возможностей и фиксинга бойлерплейта.

Сам по себе редакс — просто очень примитивная технология без всего этого.

А можно просто не страдать фигней, переместиться в наше время и использовать MobX, жизнь станет на порядок проще, код станет на порядок лучше.

Собственно одна из особенностей MobX заключается в том, что он также не имеет инструмента для control flow =) А вот эффектор лишен этого недостатка.

Собственно одна из особенностей MobX заключается в том, что он также не имеет инструмента для control flow =) А вот эффектор лишен этого недостатка.

Конкретный кейс пожалуйста приведите, дабы не быть голословным где все рушится и нам ничего не остается кроме как использовать Effector. Вдруг и правда все так плохо и придется переходить на него.
Ясно, конкретный кейс вы разумеется не приведете, потому что его нет. Очередные пустые слова, основанные на «я где-то читал, кто-то обмолвимся и т.п.» не имеющие отношения к реальности.
Не рассчитывайте на useSelector в плане оптимизаций. Он лишь предотвращает ререндер, если сравнение результата через === вернуло true.

Вот пример, где селектор возвращает простую, но структурированную информацию о пользователе:


{
   name: 'John',
   email: 'john@company.com',
   address: {
      state: '',
      city: '',
      zipCode: '',
   }
}

И здесь shallowCompare вторым аргументом уже не работает.


Это в редаксе+реакте, на самом деле жутко раздражает. Нужна мемоизация либо внутри селектора, либо мемоизировать циклы рендера. Тогда как mobx этой проблемы не имеет вовсе.

Хорошая статья, вы много чего очень правильно рассказали несчитая нескольких мелких нюансов.

Re-reselect пытается обобщить решение теоретической проблемы для которой скорее всего найдется более очевидное и простое решение в конкретном случае.

В композиции селекторов нужно найти баланс, нет смысла за-DRY-ивать и усложнять код который состоит из одной строки. Иногда лучше продублировать путь ещё раз чем вкладывать функции. Это упростит чтение.

counterSelectors.count ничем не короче писать в итоге чем selectCounterCount. Вот только второй вариант можно удобно автоимпортить и он рекомендуется стайлгайдом.

Надеюсь этот комментарий полезен :)

counterSelectors.count ничем не короче писать в итоге чем selectCounterCount. Вот только второй вариант можно удобно автоимпортить и он рекомендуется стайлгайдом.

Это вопрос семантики и внешнего/внутреннего интерфейса.


Внутри файла нет никакого смысла уточнять к чему конкретно относится селектор — к модулю counter, или, например, users. Мы и так из контекста имеем об этом информацию. С припиской же, внутренний интерфейс становится переусложнен — имена более длинные и имеют больше визуального мусора, который не несет никакой ценности.


Другими словами, внутренний интерфейс пытается решить проблему внешнего, и из-за этого сам теряет удобство. В принципе, здесь можно провести аналогию с модулями: внутренний модуль пытается решить задачу своего родительского модуля, о котором он вообще не должен знать.


Используя scope/namespace/объект, мы решаем проблему на уровне внешнего интерфейса и не трогаем при этом внутренний.


Насчет "ничем не короче писать". Ну, я и не пытался сводить это к количеству символов. Мне кажется, логичнее группировать вещи, которые относятся к одной и той же сущности. Данный принцип используется везде, не только в программировании, и понятен человеку подсознательно.


Насчет "удобно автоимпортить". Не очень понял проблему. Мы знаем, что селектор относится к модулю counter, и знаем, как у нас именуется экспорт с селекторами. Пишем counterSelectors — редактор предлагает автоимпорт, и мы его делаем. После, нажимаем точку и видим все селекторы, относящиеся к данному модулю.


Кстати, а что за стайлгайд? README.md у reselect?

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


// counter/model/index.js
export * as counterSelectors from './selectors';

Когда напишем counterSelectors в новом файле редактор ничего не подтянит так как экспорта с таким именем нет, чтобы подтягивал нужно объеденять все в объект counterSelectors и именуемый экспорт делать, что практически сводит на 0 ведь подход. Имя домена будет упомянуто 1 раз, это преимущество, но за это прийдется платить добавлением каждого селекта в объект counterSelectors для экспорта.


Если у тебя это как-то работает и так поделись секретом :)

Собственно эта строчка и делает экспорт в виде объекта) Там внутри все селекторы. В смысле не редактор ничего не подтянет?)

Да ты прав, это все моя невнимательность, я недосмотрел что это експорт

Я об этом
https://redux.js.org/style-guide/style-guide#name-selector-functions-as-selectthing


Понимаю твою мотивацию, не говорю что делать так как ты предлогаешь это не правильно, я так же делал одно время, лень было упоминать имя slice в каждом селекторе. Но сейчас я предпочитаю объявлять все селекторы в файле для slice. И то что оно в одном файле никак не мешает наоборот удобно так как все что касается этого слайса в одном месте.


export const selectPLPData = (state) => state.plp.data;
...
createSlice({
  name: ''plp",
  ...

slice ведь и так знает что он 'plp', так что никакой проблемы с внешним и внутренним конкретно в именовании нет.


Вообще это достаточно незначительный вопрос именования, я просто предпочитаю делать то, что говорят в доке

Эта рекомендация вообще для новичков. В доке очень много плохого кода.

В больших приложениях с таким именованием и подходами все превращается в мусорку.

Я согласен, что когда есть селекторов на 100 строк, то держать их в файле со slice может быть неудобно и тогда, возможно твой подход имеет смысл. Но если у меня 11 довольно простых селекторов на 25 строк, я знаю что супер сильно их количество не увеличится, то отдельный файл как-бы и не нужен. Не знаю как тебя, а меня бесит когда куча файлов по 10 строк. Я считаю что создавать папки и выносить в отедельные файлы имеет смысл когда кода действительно становится много (или если это очевидно сразу). Это ведь можно сделать без сайд эффектов для остальной кодовой базы.


доке очень много плохого кода

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


Эта рекомендация вообще для новичков

Ну как бы нет, там такого не написано, style-guide он для всех


таким именованием и подходами все превращается в мусорку

О чем это ты? С чем-то еще не согласен из redux style-guide или просто не согласен со мной?

Sign up to leave a comment.

Articles

Change theme settings