Pull to refresh

Comments 30

Когда я вижу код на реакте, мне хочется его отрефакторить, но я не могу его отрефакторить, так как нужно рефакторить сам реакт react.

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


  1. Объединение store, actions (лучше их называть store modificators, чтобы не путать с редаксовыми) и selectors в единый слой. Те данные и методы, с которыми работают фронтендеры — не микросервисы и не игровые сущности, для которых группировка подходит четко, и Dog может run() и можно get condition() исходя из внутренних параметров. АПИ своего бэка или стороннее может быть так спроектировано, что содержит смешанные данные, которые придется разложить по десятку семантических сторов, поэтому фича редакса с возможностью по одной константе отлавливать payload в разных подсторах очень востребованна.
    Касательно селекторов — то же самое, часто необходима комбинация данных из нескольких семантических сторов, и get canShowSomeBlock не будет принадлежать ни стору User, ни UI, ни Forms, а данные, необходимые для вычисления данного параметра, могут браться из них всех. И тут на сцену выходит либо Dependency Injection со всей сопутствующей кашей и полным уничтожением семантичности и консистентности хранилищ, либо все же создается отдельный слой.
    Если же модификаторы и селекторы признать отдельным слоем и передавать в них весь store со свободой модификации и чтения данных из любого, то получаются простые для понимания и использования слои с собственной структурой, продиктованной не архитектурой хранилищ, а реальными вариантами использования. За счет передачи всего стора получается удобная группировка без необходимости в модификаторах делать ручные DI и вызывать многочисленные методы.
    Из недостатков описанного мной подхода — возможность раздувания этих функций-модификаторов хранилища, что решается более узкой их специализацией. Также многим, привыкшим к структурам типа Dog.run() не понравится отсутствие "явного разделения ответственности", когда функция может получать доступ к любому параметру без жестких ограничений, как и реактовые компоненты (через контекст) к любому экшену-модификатору или опять же параметру стора, что якобы дает разработчику "свободу действий", что обязательно выльется в лапшеобразный код с неявными взаимосвязями. На деле такое встречается намного чаще как раз при наличии ограничений, которые разнообразными хаками (DI, горизонтальные связи, параллельные системы доступа к данным, дубляж) преодолеваются и код становится не только лапшеобразным, но и неподдерживаемым — при желании его отрефакторить сломаться может что угодно. В случае же, если некий экшен, написанный юниором, использует данные из 4 подсторов сразу, хотя его можно было бы разбить на 2 более семантичных экшена, это рефакторится за минуты фактически без рисков поломки, и не является легаси, в отличие от хаков, обходящих основной архитектурный паттерн.


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


    const getUser = ({ store, api }) => {
        return api.getUser().then((user) => runInAction(() => {
            store.user = user;
        }));
    }

    я не вижу. Если же данные юзера могут приходить и в других ручках, то можно создать более универсальный модификатор setUser = user => partialAssign(store.user, user), но не считаю это обязательным изначально, так как выглядит преждевременной оптимизацией и в общем случае приведет к созданию десятков лишних функций.
    А сам сбор данных из сторонних источников в модификаторах считаю оправданным, так как иначе придется выделять еще один слой "подготовки данных перед вызовом модификатора", который всегда будет вызываться перед самим модификатором и в 99% случаев это будет дубляж. Оставшийся процент — кейсы, когда источником данных может служить не одна сторонняя система, а несколько, но это проще решить if условием в самом модификаторе, чем менеджерингом отдельного слоя предварительной нормализации данных. Либо можно таким вот promise-middleware паттерном


    const getMixedData = ({ actions, api }) => {
        return api.getDataWithPartialUser()
            .then(actions.setUser)
            .then(actions.setAnotherData)
    })

Спасибо за развернутый комментарий!
Завтра-послезавтра выложу вторую часть с примерами кода.
Будет виднее, как описанное выглядит в коде и к чему может привести.
К тому же, там рассматривается часть затронутых вами моментов.

После этого я прокомментирую спорные моменты.
Теперь прокомментирую спорные моменты.
Объединение store, actions (лучше их называть store modificators, чтобы не путать с редаксовыми) и selectors в единый слой.
Я тоже предпочитаю отделять данные от функционала, когда это того стоит. Тут же надо еще подумать, как отделить без усложнения разработки.
Кстати, отмечу, что данные у меня не совсем смешаны с функционалом. При создании стора есть привязка функционала к типу данных, но нет привязки функционала к конкретному экземпляру данных. То есть у самой структуры Dog нет методов. Store просто хранит эту структуру и имеет методы для работы с ней. В сторе можно в любой момент заменить текущий экземпляр dog на другой. Если надо с ней как-то по-другому работать, можно завести новый тип стора для этого же типа данных Dog, тогда для второго экземпляра dog будет существовать отдельный функционал.

АПИ своего бэка или стороннее может быть так спроектировано, что содержит смешанные данные, которые придется разложить по десятку семантических сторов, поэтому фича редакса с возможностью по одной константе отлавливать payload в разных подсторах очень востребованна.
Может в таких проектах и будет востребована. Я не часто с таким сталкивался, и в таких ситуациях просто в действии контроллера (аналог middleware в redux) последовательно вызывал методы обновления сторов.
Так-то описанный вами случай подходит под то, что я упомянул в статье
«Я полагаю, что события можно использовать для случаев, если:
нужно избавиться от большой связанности. Например, когда один (либо несколько) объект должен вызывать схожий метод у множества других объектов.»


Касательно селекторов — то же самое, часто необходима комбинация данных из нескольких семантических сторов, и get canShowSomeBlock не будет принадлежать ни стору User, ни UI, ни Forms, а данные, необходимые для вычисления данного параметра, могут браться из них всех.
В таком случае я бы при необходимости добавлял вычисляемую функцию между сторами и компонентами. Например, можно воспользоваться функцией computed из MobX:
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';

const computedValue = computed(() => {
  return storeA.value + storeB.value;
});

const MyComponent  = observer(() => {
  const value = computedValue.get();
  ...
});


По поводу того, что в модификаторах не стоит вызывать асинхронную логику получения данных из сторонних источников.
Этот момент я показал во второй статье. Отчасти сводится к тому, что за счет разделения ответственности можно добиться меньшего количества кода. Хоть добавляется слой controller, не придется для каждой страницы создавать свой тип стора и контроллера, либо писать методы с повторяющимся кодом. Вместо этого во многих случаях подойдут базовые реализации. Если откроете файлы контроллеров, сторов в этих двух страницах:
github.com/sergeysibara/mobx-react-4-layer-architecture/tree/wrap-feature/src/pages
то увидите, что там нет нового функционала. Только объявление типов, с которыми они должны работать.
Ну и другой момент. Если писать асинхронный код, как обычно пишут в MobX, где писать остальные сайд-эффекты, которые не относятся к компонентам? Зачастую их мало и уместно писать прямо в сторе. А вот для проектов, где сайд-эффектов много?
Так-то я согласен с вами, что надо смотреть по ситуации и если не видно пользы, то и усложнять не нужно.
UFO just landed and posted this here
Только собственные методы связанные с хранением данных в сторе.
Вот я тоже за такую логику и как раз, так и делаю. Только в комментариях ко второй части статьи пишут, что не нужно усложнять и достаточно MVVM, где компонент вызывает метод стора, а стор обращается к api сервису и прочим сервисам.

Не должно быть другого стора.
Не понял, что вы имеете в виду?

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

Что за бред. В сторе нужны функции которые фетчат данные и сохраняют к себе, они должны лежать вместе. Нельзя все это разносить, одно без другого нафиг не нужно.
Стор не должен быть ни частью mvc, ни брать на себя их функции. Только собственные методы связанные с хранением данных в сторе.

С чего вдруг? Астрологи такой прогноз нагадали?
Хотел Вас спросить: есть ли в MobX аналог connect?
То есть функция которая генерирует страницу имеет шапку
export const Home = ({ num, changeNum }: any): any => {
где num это значение из стора, а changeNum — соответствующая функция. Что мне нужно сделать в корневом компоненте, чтобы оно заработало? просто не хочу загромождать страницу MobX специфичными конструкциями, гораздо лучше чтобы странице было все равно какой у нас стор.
Конструкция
function App() {
  const [state] = useState(() => new LocalState())
  const HomePage = observer(() => {
    const Page = Home({ num: state.num, changeNum: state.handleChangeNum })
    return <Page />
  })
  return (
    <>
      <HomePage />
    </>
  )
}
не работает, говорит, что Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

Нет, аналога connect из коробки нет, но написать его несложно. Однако, надо следить, чтобы не знающему про Mobx компоненту не пришёл observable-объект (собственно, это и есть причина того почему аналога connect нет).


Теперь по вашему коду. Что вы вообще понаписали?


Во-первых, почему столько any?


Во-вторых, судя по описанию, Home возвращает элемент (JSX.Element), а вы его присваиваете в переменную Page и используете как будто это компонент. Кстати, если бы не any, этой ошибки бы было.


В-третьих, вы создаёте новый компонент (HomePage) при каждом рендере App. Ну нафига так делать, если для подобных случаев придуман компонент Observer?


Правильный код будет выглядеть как-то так:


function App() {
  const [state] = useState(() => new LocalState())

  return (
    <>
      <Observer>{() => 
        <Home num={state.num} changeNum={state.handleChangeNum} /> 
      }</Observer>
    </>
  )
}

Или вот так:


@observer
function HomePage({ state: LocalState }) {
    return <Home num={state.num} changeNum={state.handleChangeNum} />
}

function App() {
  const [state] = useState(() => new LocalState())

  return (
    <>
      <HomePage state={state} />
    </>
  )
}
По поводу кода — посыпаю голову пеплом, за пол года MobX (да и React) как-то выветрился из головы.

В компонент Home num и changeNum все равно попадут как props?

Про observable-объект не понял: если у меня num определен как
@observable num: number
то в код любом случае не будет работать?

Это как раз работать будет. А вот если компоненту Home передать store...

Тогда подскажите, пожалуйста, где я сфейлил — судя по выводу консоли num в сторе увеличивается, но страница не знающая про MobX упорно этого не видит codesandbox.io/s/sweet-ellis-45odh

Вы забыли makeObservable(this); в конструкторе написать.

Интересно, что при изменении дочерней структуры this.auth.username = something и передачи родительского state.auth в качестве пропса реактивность не работает, а вот при передаче любых дочерних структур работает на ура, это так должно быть?

Любой observer подписывается на те observable, которые он читает. Если вы читаете и передаёте store.auth — вы подписываетесь на изменение свойства store.auth, но не подписываетесь на изменение store.auth.username.

Если нужно реагировать на изменения во всех дочерних структурах, можно использовать метод toJS из MobX, пример


reaction(() => toJS(state.auth), function onAnyChangeInAuth() {
 ...
})

Через пропсы передавать с помощью toJS крайне нежелательно, так как родительский компонент будет ререндериться, когда это не нужно. В целом ситуации, когда этот механизм нужен, крайне редки — в голову приходит только синхронизация стейта из оперативной памяти и какого-нибудь persistent storage, того же localStorage, или онлайн-игр с сохранением состояния в Firebase. То есть при любом изменении в хранилище оно целиком (в реальности, конечно, через дифф-алгоритм) будет записываться в постоянное хранилище и не будет пропадать при перезагрузке страницы.

Пожалуйста, читайте ветку полностью. Он создаёт отдельный компонент-обёртку, в котором находится только один дочерний. В такой ситуации использовать toJS в пропсах — прекрасное решение.

Ну в компоненте auth явно не только username и как я уже почувствовал — в пропсах лучше передавать конкретные параметры, а не ветку целиком — спокойнее и надежнее

Чем же оно прекрасное, зачем компоненту-обертке лишний раз ререндериться? И конкретные параметры тоже лучше не передавать, опять же, это нивелирует бенефиты от observable. Куда лучше в самом этом дочернем компоненте использовать store.auth.username — тогда только он и обновится. Реактивность работает именно так, как должна.

Если использовать store.auth.username в компоненте, который не является observer — он вообще не будет обновляться.

Это-то тут причем? Конечно, он должен быть обернут в observer, разве об этом разговор? export const Home = observer(...).

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

Я посчитал, что это было банальной ошибкой в коде, так как смысла не оборачивать компонент не вижу, в комментарии слишком мало контекста и явно плохое понимание как работает observable. Если этот Page — компонент из сторонней библиотеки, то достаточно передать отдельные props, но тут еще упоминалась передача "state.auth в качестве пропса", так что это явно внутренний компонент.


В целом ветка с перепутанными смыслами, неявным контекстом и мешаниной подходов, поэтому я написал комментарий не к ветке, а в общем — что есть механизм toJS, но актуальность его использования крайне редка — есть решения лучше (обернуть дочерний в observable или передавать отдельные пропсы если компонент сторонний). Ваш вариант с toJS некой объемной структуры данных, часть из которых может быть не нужна в дочерних и вызовет лишние ререндеры плохой в любом случае.

Надо было сразу давать ссылку, описывать код по-русски дело неблагодарное.
Библиотеки не при чем все компоненты и стор внутренние. Frontend у меня действительно идет как-то грустнее, чем бэкенд, просто пытаюсь mobx применить не топорно а более обдуманно — вместо redux-а.
И как нуб в mobxя действительно не понимаю как именно работает observer и буду рад хорошей ссылке.

А там нечего понимать, "любой observer подписывается на те observable, которые он читает" — это единственное что требуется знать.

Мне кажется автору стоит перечитать принципы SOLID. Они относятся к ООП, а не к ФП. Для React выбрал бы скорее redux, чем Mobx. Для меня лично, обычная среда применения React небольшие и средние приложения. В подобных проектах много неопытных программистов, соответственно redux ведет их по своей простой архитектуре, позволяя избегать многих ошибок. Да и легковесность манипуляций данных в redux позволит в небольших проектах получить лучшую отдачу. В крупных проектах предпочел бы mobx, особенно касаемо проектов связанных с данными в виде дерева. Да и плюс использовал бы не React, а Аngular. Ему философия Mobx ближе)
Почему вы думаете, что эти принципы ограничены только ООП? Некоторые принципы проектирования из одной парадигмы в том или ином виде могут быть применимы и к другим.
Для большей убедительности:
twitter.com/unclebobmartin/status/1164511654618550273
Sign up to leave a comment.

Articles