В целом поддерживаю, только Babel на мой взгляд уже давно не нужен
А как же гибкость? У бабела хорошая база transformation плагинов и их апи пока лучше, чем в ts.
В 7м бабеле (пока еще бета) запилили поддержку синтаксиса typescript: babel-preset-typescript. Так что можно сказать, что и ts для целей сборки не нужен.
Интересно, что будет, если в flow запилят поддержку ts и плагинов для Language Service API?
Что-то раз от раза меньше материала по Angular становится, он популярность теряет или что?
Это наверное первый дайджест за последнее время, где о нем вообще ни слова, кроме тега.
1. От connect и подписки componentWillMount в коде приложения можно избавиться, переопределив createElement, который будет создавать обертку с подписками вокруг исходного компонента
Обертка createElement по deps найдет Store, проинжектит в контекст. Идея в том, что если вам надо прокидывать до HelloView сторы транзитом сквозь кучу компонент, просто перенесите его в контекст. Т.е. контексты в приоритете над пропсами. Если надо переопеделить Store, то можно использовать что-то вроде:
actions в MobX — нужен из-за оптимизации синхронной природы обновлений, в решениях с асинхронными обновлениями можно и без них. Однако actions позволяет лучше генерировать лог изменений стейта, когда понятно кто изменил состояние, это важнее. Также undo/redo можно построить на них.
Про наблюдаемый объект parentObj.level1Obj = observable({level2Obj: {propA: 10}}); вам ответили, что не обязательно в данном случае заворачивать в observable.
Для демонстрации работы с контекстами и обработки ошибок приведу пример на reactive-di.
Гибкий, наверное имелось в виду расширяемость компонент хорошая и решение многих ситуаций со стейтом и загрузкой уже есть в ядре.
Но да, в mol все свое, основная идея — что все части очень хорошо пригнаны друг к другу. Из коробки сразу все есть, включая UI-библиотеку. Если нужны бутстрапы и пр. — это не к mol.
подвергая себя зависимости от одного единственного
Справедливости ради отмечу, что Винтаж упорный и активно пилит этот мол уже который год. Кстати, первые атомы, которые еще jin, он запилил до MobX, по статьям видно.
А вот с популяризацией своих творений у автора что-то не ладится.
У вас хорошие оригинальные идеи и много технически неплохих статей. Но зачем вы выбираете такой агрессивный стиль подачи, что в статьях, что в комментах?
Пожалуйста, уберите эмоционально окрашенный маркетинговый булшит, он не несет полезной нагрузки. Это не помогает лучше понять материал, скорее наоборот, вызывает отторжение и негатив.
Думаю, как раз не легко. Не только в экшенах дело. Как, например в голом mobx узнать что изменилось в транзакции и получить патч изменений. Как генерить патчи без дупликации данных.
Пытаясь решить эти задачи, тащат и описание типов в run-time и описание ссылочности между ними и кучу всего, что уже сложно замаскировать под обычные объекты с декораторами.
Здесь не знаю, но например в mobx-state-tree есть аналогичные actions. Их суть в группировке операций по изменению стейта. Таким образом можно регулировать атомарность записи в лог транзакций. Всякие undo/redo работают с группами базовых операций по изменению стейта.
Интересно было бы добиться такой же группировки на экшенах, с сохранением нативных классов, не наворачивая всяких фабрик поверх. Иными словами, как получить фишки редукса и mst без «very specific state architecture».
Идейно может это и классная штука.
Но с виду проект полудохлый, активность низкая, та issue с Maximum call stack (229) висит второй год и перечеркивает продакшен использование волокон.
Упоминая полудохлую технологию, работающую только на ноде и которую вряд ли когда примут в стандарт, что вы пытаетесь показать? Что она удобнее async/await, ну может быть, но знание этого ничего не дает — оно бесполезно.
let val = { foo : [777] }
let called = 0
class A {
@mem foo(next?: Object): Object {
called++
if (next === undefined) return undefined
return val
}
}
function assert(a, b) {
if (a !== b) throw new Error('Assert error: ' + a + ' != ' + b)
}
const a = new A()
assert(a.foo(), undefined)
assert(called, 1)
a.foo({foo: [666]})
assert(called, 2)
assert(a.foo().foo[0], 777)
a.foo({foo: [666]})
assert(called, 2)
assert(a.foo().foo[0], 777)
a.foo({foo: [777]})
assert(called, 2)
assert(a.foo().foo[0], 777)
assert(called, 3) вызовется 3й раз, если мы засетим значение {foo: [777]}, а по-логике не должен, т.к. это значение выставит хэндлер.
В общем то да, можно на mobx сделать и аналогичную логику. Только вот как это автоматизировать, что б не было шаблонного кода. Мне эта задача не кажется простой.
Идея mobx все же немного другая — это достаточно низкоуровневый конструктор с кучей хелперов, из которых можно гибко выстроить сложную кастомную логику.
Атомы — это больше соглашения, приняв которые, можно с минимумом бойлерплейта решать типовые задачи в веб без привнесения хелперов, без ручного программирования деталей таких вот крайних ситуаций.
Все же сложновато их сравнивать с аналогичной псевдо-оберткой над mobx.
Может я что-то не так понял, пример бы многое прояснил. Сложность то никуда не девается, если не в свойствах, то где-то рядом будет накручена. Реакции и перехватчики могут запросто модифицировать стейт как угодно, совершенно непредсказуемым образом с точки зрения внешнего наблюдателя.
Зачем городить эффекты, экшены, реакции, выстреливать store.fetchMessage() в componentDidMount, если достаточно просто обратиться к свойству store.message, в котором сработает логика обновления этого свойства.
Под объект мы маскируем не только реактивность, но и канал связи. Это сильно связанные задачи, код упрощается за счет автоматизации типовых задач.
Вот пример, который асинхронно загружает данные, обрабатывает ошибки и рисует статус загрузки:
Пример загрузки на lom №1
// ...
function fetchMessage() {
return new Promise(resolve => {
setTimeout(() => resolve('message'), 1500)
})
}
class Store {
@force $: Store
@mem set message(next: string | Error) {}
@mem get message() {
fetchMessage()
.then(message => { this.$.message = message })
.catch(error => { this.$.message = error })
throw new mem.Wait()
}
}
function HelloView({store}) {
return <div>
<input
value={store.message}
onInput={({target}) => { store.message = target.value }}
/>
<br/>{store.message}
</div>
}
const store = new Store();
ReactDOM.render(<HelloView store={store} />, document.getElementById('mount'));
Это другой случай. Идея в том, что мы предлагаем значение, а хэндлер уже решает, что с ним делать. В моем примере хэндлер отклонял значение и записывал свое — { foo: [777] }.
reaction не позволяет модифицировать записанное значение.
Кстати, а такой код утечки памяти не создает? reaction возвращает disposer.
Можно много ситуаций привести, где такой подход плохо себя проявит. Это происходит обычно на средних и больших масштабах приложений.
Например, нарушение инкапсуляции — данные сегодня были захардкожены, а завтра их решили получать с сервера (список юридической отвественности, например АО, ИП и т.д.). Факт того, что данные асинхронны, протекает в компоненты в виде вызовов fetchSome в componentDidMount и в виде структур данных {status, value, refresh}. Т.к. заранее предугадать это было нельзя, то надо рефачить компонент, который мог использоваться в 10 проектах, соотвественно они будут затронуты со всеми вытекающими.
Дублирование кода — у вас одни и те же данные могут быть в разных компонентах использованы, получается в каждом надо вызывать componentDidMount. Иначе нет гарантии, что данные актуальны.
Нарушение единственной ответственности (SRP). Основная задача компонента — генерить верстку. Но реактовый компонент в виде класса — это все вместе: стейт, логика и верстка, т.е. у него много отвественностей. В небольших масштабах это удобно — просто и все под рукой. Однако с ростом кодовой базы много проблем возникает в поддержке и кастомизации, как в вышеописанном примере.
Работа с данными и сервером — зона ответственности слоя данных, моделей, например. Неважно кто запросил данные, сам факт запроса означает, что данные надо вытянуть с сервера, если они не актуальны. Правильное распределение отвественности позволяет избавиться от дублирования кода за счет автоматизации. Актуализация становится автоматической, код упрощается.
В свете такого разделения отвественности, компонент реакта должен быть только тупой. Во всяких Vue, Angular-ах, WebComponents, верстка — отдельная сущность. При проектировании реакта не заглядывали так далеко, он был нужен для быстрой разработки небольших приложений и с этой задачей хорошо справляется, хоть и не соблюдает все эти мудреные принципы.
Ну и делать соответствующие проверки в компоненте.
Присмотритесь к примерам в статье на lom_atom, там в компонентах проверок нет вовсе, как будто нет асинхронных запросов к серверу. При этом статусы и ошибки обрабатываются.
В принципе можно через lazyObservable, но как это сделать без оберток над данными, не усложняя код?
Надо просто выдерживать одинаковый интерфейс. Механика зависимых свойств позволяет это делать.
Вот слово «просто» бы раскрыть, какой это будет интерфейс? Если это интерфейс вроде {status, value, refresh}, предметная область будет замусорена такими обертками поверх данных.
Будут. Работают.
Какая-то не конструктивная беседа получается, давайте конкретнее, может добавите пример какой-нибудь?
Как в mobx предложить значение? Мы его записываем в observable-свойство, но в реальности оно попадает в функцию, которая решает что с ним делать, вместо него записывает нормализованное значение и сохраняет на сервер.
Пример на lom_atom
let val = { foo : [777] }
let called = 0
class A {
@mem foo(next?: Object): Object {
called++
if (next === undefined) return undefined
// save to server
return val
}
}
// ...
const a = new A()
assert(a.foo(), undefined)
assert(called, 1)
a.foo({foo: [666]})
assert(called, 2)
assert(a.foo().foo[0], 777)
a.foo({foo: [666]})
assert(called, 2)
assert(a.foo().foo[0], 777)
fiddle
Вот, что я смог придумать на MobX и он вовсе не оптимизирует второе присвоение с тем же значением, called будет 3, а не 2 в конце. В случае сохранения на сервер, это означает что каждый раз будет вызываться тяжелая логика «save to server» с одним и тем же значением:
Пример на mobx
let val = { foo : [777] }
let called = 0
class A {
@observable foo: Object
constructor() {
intercept(this, 'foo', (change) => {
called++
if (change.newValue) {
// save to server
change.newValue = val
}
return change
})
autorun(() => {
;this.foo;
called++
})
}
}
// ...
const a = new A()
assert(a.foo, undefined)
assert(called, 1)
a.foo = {foo: [666]}
assert(called, 2)
assert(a.foo.foo[0], 777)
a.foo = {foo: [666]}
assert(called, 2)
assert(a.foo.foo[0], 777)
Про реактивность на атомах у Дмитрия есть теория. Проталкивание, вытягивание, двусторонние каналы, вот это вот всё.
MobX не реализует эту теорию, он пошел по пути развития хелперов над достаточно простым ядром, которое работает с данными, а не хэндлерами. В его основе нет вышеназванных понятий. Я считаю, что реализовывать эту теорию поверх mobx — это как каша из топора, будет надстройка по-сложности сравнимая с самим mobx. Это не путь mobx. Если у вас есть понимание, как это сделать просто — попробуйте, всем будет интересно.
Если речь идет о экшенах/эффектах, это ближе к ФП, с его плюсами и минусами. MobX же и атомы — это более привычный многим ООП. Что-то проще делается в одном, что-то в другом, но развивать можно оба направления.
Условно можно сказать, что тут эффекты внутри самого свойства. По сути, это спецификация, позволяющая с минимальным бойлерплейтом описывать observable-свойства с эффектами и управлять этими эффектами. Накрутить в них тротлинг не проблема, также как и сделать отмену. Вообще отмена автоматически происходит, когда компоненты больше эти данные не выводят.
Зачем диспатчить экшен, когда можно просто изменить свойство в объекте, если конечно вам не нужно версионирование и фишки, вроде time travel. Экшены можно накручивать поверх, как например в mobx-state-tree.
Лучше накидайте пример на псевдофреймворке, как вы себе это представляете, а я попробую накидать аналогичный, на lom.
destructor косвенно вызывает компонент в componentWillUnmount. Также, если компонент перерендерился в очередной раз и данные эти не запросил.
Если вызвать из приложения напрямую — будет плохо, закроется ресурс, это также как у результата fromResource dispose вызвать. К сожалению, пока не понятно, как это инкапсулировать, не усложняя абстракции.
Иными словами, к такому виду привел поиск универсального минимального решения. Кстати первого не меня, а vintage, я пробовал реализовать аналог по-своему и на обсерваблах. Но отказался от этой идеи, Дмитрий убедил меня. Есть много нюансов, например, непонятно как с Observable форсировать обновление. Если интересно, есть epic issue, где я пробовал аргументировать разные варианты.
Это детали реализации fromPromise. Никто не запрещает вам сделать свой вариант который бы кидал исключение.
Как минимум, не получится сделать асинхронную неблокирующую загрузку, т.к. mobx не перехватывает внутри исключения и не преобразует их в Proxy. Не будут работать некоторые оптимизации обновления состояния, например, в ситуации, когда мы записываем значение в свойство, а хэндлер его отвергает и возвращает свое, мы снова записываем первое значение, в этот момент не должно вызываться обновление компонента. Как в этом тесте.
Общий приницип реализовать наверное можно, вопрос какой ценой. Да и зачем, если связку observable, computed, fromPromise, autorun, можно заменить на один mem, да и благодаря асинхронной природе модификации состояния атомов, actions не нужны ради оптимизации обновлений стейта.
Потому что их обрабатывает какая-то магия на стороне вида. Добавляем такую же магию в mobx — и все работает.
Вообще, может быть, будет время — поэкспериментирую. Но далеко не факт, кажущаяся простота, однако когда начинаешь делать, всплывает много деталей. Различие в обработке исключений может стать препятствием.
Вот вы и нашли решение.
Я все пытаюсь объяснить про интерфейсы, в идеале стоит стремиться к тому, что бы детали способа получения данных не протекали в компоненты. lazyObservable возвратит данные, запакованные в метаданные. Метаданные — детали канала связи, как вот тот refresh. С хорошей абстракцией, код TodoListView не должен меняться, если данные в todoList.todos были сперва захардкожены, а потом его решили получать с сервера. Идеального решения тут пока нет, но в lom_atom это свойство любого mem-значения, не нужно специально использовать хелпер, подобный lazyObservable.
А в чем, собственно, проблема?
Кроме вышесказанного могу добавить, что у mobx не было целей делать подобное в ядре, это библиотека просто работает с реактивным стейтом, не учитывая канал связи. Это неизбежно выливается в усложнение вышележащего уровня — хелперов. Усложнение как их реализации, так и их использования в прикладном коде.
Примитивность основы плохо сказывается на целостности экосистемы — появляется много решений, которые частично дублируют функциональность друг друга. Я исхожу из предположения, что кроме реактивности, абстракция работы с каналом связи — должна быть фундаментальным свойством фреймворка (а лучше языка). Эти вещи тесно связаны.
Ее в любом случае надо написать, независимо от используемой библиотеки, потому что эти танцы с force выглядят как «О_О что тут вообще происходит?!»
Я преследую цель — придумать спецификацию, которая бы не зависела от внешних библиотек или хелперов, не усложняла бы интерфейс чистых классов. Force не самое удачное решение, но в этом смысле он менее всех захламляет предметную область. Может придумаю что-нибудь получше.
то fromResource из mobx-utils подходит идеально. Как раз под эту задачу создавался.
Вообще мне не очень понятно разделение на lazyObservable и fromResource. Они частично копируют функциональность друг друга, при этом каждый что-то свое добавляет. Оба работают с даннымми в схожем стиле через sink, при этом в lazyObservable — можно отслеживать статусы и делать refresh, но нельзя задать unsubscriber, fromResource — можно задать unsubscriber, но нельзя отслеживать статусы и делать refresh.
Так или иначе, можно добиться конечно, но ценой привнесения реализации хелпера со своей спецификацией. А если надо ослеживать статус и ошибку, придется усложнять интерфейс данных, добавляя status/error.
Сравните:
на fromResource
function createObservableUser(dbUserRecord) {
let currentSubscription;
return fromResource(
(sink) => {
// sink the current state
sink(dbUserRecord.fields)
// subscribe to the record, invoke the sink callback whenever new data arrives
currentSubscription = dbUserRecord.onUpdated(() => {
sink(dbUserRecord.fields)
})
},
() => {
// the user observable is not in use at the moment, unsubscribe (for now)
dbUserRecord.unsubscribe(currentSubscription)
}
)
}
// usage:
const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))
// use the observable in autorun
autorun(() => {
// printed everytime the database updates its records
console.log(myUserObservable.current().displayName)
})
// ... or a component
const userComponent = observer(({ user }) =>
<div>{user.current().displayName}</div>
)
на lom_atom
class UserStore {
@force $
constructor(dbUserRecord) { this.dbUserRecord = dbUserRecord }
@mem set user() {}
@mem get user() {
const {dbUserRecord} = this
const currentSubscription = dbUserRecord.onUpdated(() => {
this.$.user = {...this.$.user, current: dbUserRecord.fields}
})
return {
current: dbUserRecord.fields,
destructor() {
dbUserRecord.unsubscribe(currentSubscription)
}
}
}
}
// usage:
const myUserObservable = new UserStore(myDatabaseConnector.query("name = 'Michel'"))
// ...
// ... or a component
const userComponent = observer(({ user }) =>
<div>{user.current.displayName}</div>
)
Если закрыть глаза на force (пока), то код вполне читабелен. В отличие от fromResource те же действия выражаются нативными кострукциями языка. Мне было интересно попытаться сделать на таком принципе.
То же самое что и в lom_atom.
Не то же самое, в MobX, детали, вроде value и status просачиваются в computed и в компоненты, сравните:
class TodoList {
@force $: TodoList
@mem set todos(next: Todo[] | Error) {}
@mem get todos() {
fetchSomeTodos()
.then(todos => { this.$.todos = todos })
.catch(error => { this.$.todos = error })
throw new mem.Wait()
}
@mem get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length;
}
}
function TodoListView({todoList}) {
const unfinished = todoList.unfinishedTodoCount
return <div>
<ul>
{todoList.todos.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {unfinished}
</div>
}
fiddle
В lom_atom не надо обрабатывать случаи загрузки и ошибки в unfinishedTodoCount. Если в MobX забыть это сделать в unfinishedTodoCount, приложение сломается, что и видно в первом примере.
Тут уже из самой постановки задачи торчат уши чего-то ненормального.
Не всегда апи бывает идеальным, например в сохранили todo через POST /todo, а после надо перевытянуть весь список /todos, т.к. другого способа получить обновленную сущность с серверным id нет.
Бывают случаи восстановления после ошибки, когда пользователь нажимает на кнопку Repeat и надо заново перевытянуть данные. В том же lazyObservable есть refresh().
Вы тоже на этот вопрос ответа не дали.
Не хотел здесь это затрагивать. Тема для отдельной статьи, если сформулируете задачу, то могу на ее основе написать с примерами.
Так же как это делаете вы.
В том то и дело, автоматизировать обработку ошибок и статусов на mobx мне пока не удалось. Либо все упирается в большую сложность оберток поверх mobx. Либо уходом от mobx, небольшой доработкой базовой спецификации и гораздо меньшей сложностью реализации далее.
В 7м бабеле (пока еще бета) запилили поддержку синтаксиса typescript: babel-preset-typescript. Так что можно сказать, что и ts для целей сборки не нужен.
Интересно, что будет, если в flow запилят поддержку ts и плагинов для Language Service API?
Это наверное первый дайджест за последнее время, где о нем вообще ни слова, кроме тега.
1. От connect и подписки componentWillMount в коде приложения можно избавиться, переопределив createElement, который будет создавать обертку с подписками вокруг исходного компонента Суть в автоматизации, биндинги за сцену выносятся. Все компоненты можно чистыми оставить.
2-3. Избежать ошибки, если свойства нет, а также индикатор загрузки приделать, можно обработчиком try/catch в render обертки
4. Весь объект лучше не передавать в пропсы ради оптимизации, можно попробовать развить идею контекстов. Обертка createElement по deps найдет Store, проинжектит в контекст. Идея в том, что если вам надо прокидывать до HelloView сторы транзитом сквозь кучу компонент, просто перенесите его в контекст. Т.е. контексты в приоритете над пропсами. Если надо переопеделить Store, то можно использовать что-то вроде:
actions в MobX — нужен из-за оптимизации синхронной природы обновлений, в решениях с асинхронными обновлениями можно и без них. Однако actions позволяет лучше генерировать лог изменений стейта, когда понятно кто изменил состояние, это важнее. Также undo/redo можно построить на них.
Про наблюдаемый объект parentObj.level1Obj = observable({level2Obj: {propA: 10}}); вам ответили, что не обязательно в данном случае заворачивать в observable.
Для демонстрации работы с контекстами и обработки ошибок приведу пример на reactive-di.
За 1.5 года никто не перепроверил? Там уж несколько версий ядер сменилось и самого арча, автору пофиг?
Просто отталкивают такие вещи от использования.
Но да, в mol все свое, основная идея — что все части очень хорошо пригнаны друг к другу. Из коробки сразу все есть, включая UI-библиотеку. Если нужны бутстрапы и пр. — это не к mol.
Справедливости ради отмечу, что Винтаж упорный и активно пилит этот мол уже который год. Кстати, первые атомы, которые еще jin, он запилил до MobX, по статьям видно.
А вот с популяризацией своих творений у автора что-то не ладится.
Пожалуйста, уберите эмоционально окрашенный маркетинговый булшит, он не несет полезной нагрузки. Это не помогает лучше понять материал, скорее наоборот, вызывает отторжение и негатив.
Пытаясь решить эти задачи, тащат и описание типов в run-time и описание ссылочности между ними и кучу всего, что уже сложно замаскировать под обычные объекты с декораторами.
Интересно было бы добиться такой же группировки на экшенах, с сохранением нативных классов, не наворачивая всяких фабрик поверх. Иными словами, как получить фишки редукса и mst без «very specific state architecture».
Но с виду проект полудохлый, активность низкая, та issue с Maximum call stack (229) висит второй год и перечеркивает продакшен использование волокон.
Упоминая полудохлую технологию, работающую только на ноде и которую вряд ли когда примут в стандарт, что вы пытаетесь показать? Что она удобнее async/await, ну может быть, но знание этого ничего не дает — оно бесполезно.
На MobX:
fiddle
assert(called, 3) вызовется 3й раз, если мы засетим значение {foo: [777]}, а по-логике не должен, т.к. это значение выставит хэндлер.
В общем то да, можно на mobx сделать и аналогичную логику. Только вот как это автоматизировать, что б не было шаблонного кода. Мне эта задача не кажется простой.
Идея mobx все же немного другая — это достаточно низкоуровневый конструктор с кучей хелперов, из которых можно гибко выстроить сложную кастомную логику.
Атомы — это больше соглашения, приняв которые, можно с минимумом бойлерплейта решать типовые задачи в веб без привнесения хелперов, без ручного программирования деталей таких вот крайних ситуаций.
Все же сложновато их сравнивать с аналогичной псевдо-оберткой над mobx.
Зачем городить эффекты, экшены, реакции, выстреливать store.fetchMessage() в componentDidMount, если достаточно просто обратиться к свойству store.message, в котором сработает логика обновления этого свойства.
Под объект мы маскируем не только реактивность, но и канал связи. Это сильно связанные задачи, код упрощается за счет автоматизации типовых задач.
Вот пример, который асинхронно загружает данные, обрабатывает ошибки и рисует статус загрузки:
Кстати, lom_atom не навязывает такой подход, его можно использовать как обычный стор:
reaction не позволяет модифицировать записанное значение.
Кстати, а такой код утечки памяти не создает? reaction возвращает disposer.
Например, нарушение инкапсуляции — данные сегодня были захардкожены, а завтра их решили получать с сервера (список юридической отвественности, например АО, ИП и т.д.). Факт того, что данные асинхронны, протекает в компоненты в виде вызовов fetchSome в componentDidMount и в виде структур данных {status, value, refresh}. Т.к. заранее предугадать это было нельзя, то надо рефачить компонент, который мог использоваться в 10 проектах, соотвественно они будут затронуты со всеми вытекающими.
Дублирование кода — у вас одни и те же данные могут быть в разных компонентах использованы, получается в каждом надо вызывать componentDidMount. Иначе нет гарантии, что данные актуальны.
Нарушение единственной ответственности (SRP). Основная задача компонента — генерить верстку. Но реактовый компонент в виде класса — это все вместе: стейт, логика и верстка, т.е. у него много отвественностей. В небольших масштабах это удобно — просто и все под рукой. Однако с ростом кодовой базы много проблем возникает в поддержке и кастомизации, как в вышеописанном примере.
Работа с данными и сервером — зона ответственности слоя данных, моделей, например. Неважно кто запросил данные, сам факт запроса означает, что данные надо вытянуть с сервера, если они не актуальны. Правильное распределение отвественности позволяет избавиться от дублирования кода за счет автоматизации. Актуализация становится автоматической, код упрощается.
В свете такого разделения отвественности, компонент реакта должен быть только тупой. Во всяких Vue, Angular-ах, WebComponents, верстка — отдельная сущность. При проектировании реакта не заглядывали так далеко, он был нужен для быстрой разработки небольших приложений и с этой задачей хорошо справляется, хоть и не соблюдает все эти мудреные принципы.
Присмотритесь к примерам в статье на lom_atom, там в компонентах проверок нет вовсе, как будто нет асинхронных запросов к серверу. При этом статусы и ошибки обрабатываются.
Вот слово «просто» бы раскрыть, какой это будет интерфейс? Если это интерфейс вроде {status, value, refresh}, предметная область будет замусорена такими обертками поверх данных.
Какая-то не конструктивная беседа получается, давайте конкретнее, может добавите пример какой-нибудь?
Как в mobx предложить значение? Мы его записываем в observable-свойство, но в реальности оно попадает в функцию, которая решает что с ним делать, вместо него записывает нормализованное значение и сохраняет на сервер.
Вот, что я смог придумать на MobX и он вовсе не оптимизирует второе присвоение с тем же значением, called будет 3, а не 2 в конце. В случае сохранения на сервер, это означает что каждый раз будет вызываться тяжелая логика «save to server» с одним и тем же значением:
Про реактивность на атомах у Дмитрия есть теория. Проталкивание, вытягивание, двусторонние каналы, вот это вот всё.
MobX не реализует эту теорию, он пошел по пути развития хелперов над достаточно простым ядром, которое работает с данными, а не хэндлерами. В его основе нет вышеназванных понятий. Я считаю, что реализовывать эту теорию поверх mobx — это как каша из топора, будет надстройка по-сложности сравнимая с самим mobx. Это не путь mobx. Если у вас есть понимание, как это сделать просто — попробуйте, всем будет интересно.
Условно можно сказать, что тут эффекты внутри самого свойства. По сути, это спецификация, позволяющая с минимальным бойлерплейтом описывать observable-свойства с эффектами и управлять этими эффектами. Накрутить в них тротлинг не проблема, также как и сделать отмену. Вообще отмена автоматически происходит, когда компоненты больше эти данные не выводят.
Зачем диспатчить экшен, когда можно просто изменить свойство в объекте, если конечно вам не нужно версионирование и фишки, вроде time travel. Экшены можно накручивать поверх, как например в mobx-state-tree.
Лучше накидайте пример на псевдофреймворке, как вы себе это представляете, а я попробую накидать аналогичный, на lom.
Если вызвать из приложения напрямую — будет плохо, закроется ресурс, это также как у результата fromResource dispose вызвать. К сожалению, пока не понятно, как это инкапсулировать, не усложняя абстракции.
Иными словами, к такому виду привел поиск универсального минимального решения. Кстати первого не меня, а vintage, я пробовал реализовать аналог по-своему и на обсерваблах. Но отказался от этой идеи, Дмитрий убедил меня. Есть много нюансов, например, непонятно как с Observable форсировать обновление. Если интересно, есть epic issue, где я пробовал аргументировать разные варианты.
Как минимум, не получится сделать асинхронную неблокирующую загрузку, т.к. mobx не перехватывает внутри исключения и не преобразует их в Proxy. Не будут работать некоторые оптимизации обновления состояния, например, в ситуации, когда мы записываем значение в свойство, а хэндлер его отвергает и возвращает свое, мы снова записываем первое значение, в этот момент не должно вызываться обновление компонента. Как в этом тесте.
Общий приницип реализовать наверное можно, вопрос какой ценой. Да и зачем, если связку observable, computed, fromPromise, autorun, можно заменить на один mem, да и благодаря асинхронной природе модификации состояния атомов, actions не нужны ради оптимизации обновлений стейта.
Вообще, может быть, будет время — поэкспериментирую. Но далеко не факт, кажущаяся простота, однако когда начинаешь делать, всплывает много деталей. Различие в обработке исключений может стать препятствием.
Я все пытаюсь объяснить про интерфейсы, в идеале стоит стремиться к тому, что бы детали способа получения данных не протекали в компоненты. lazyObservable возвратит данные, запакованные в метаданные. Метаданные — детали канала связи, как вот тот refresh. С хорошей абстракцией, код TodoListView не должен меняться, если данные в todoList.todos были сперва захардкожены, а потом его решили получать с сервера. Идеального решения тут пока нет, но в lom_atom это свойство любого mem-значения, не нужно специально использовать хелпер, подобный lazyObservable.
Кроме вышесказанного могу добавить, что у mobx не было целей делать подобное в ядре, это библиотека просто работает с реактивным стейтом, не учитывая канал связи. Это неизбежно выливается в усложнение вышележащего уровня — хелперов. Усложнение как их реализации, так и их использования в прикладном коде.
Примитивность основы плохо сказывается на целостности экосистемы — появляется много решений, которые частично дублируют функциональность друг друга. Я исхожу из предположения, что кроме реактивности, абстракция работы с каналом связи — должна быть фундаментальным свойством фреймворка (а лучше языка). Эти вещи тесно связаны.
Вообще мне не очень понятно разделение на lazyObservable и fromResource. Они частично копируют функциональность друг друга, при этом каждый что-то свое добавляет. Оба работают с даннымми в схожем стиле через sink, при этом в lazyObservable — можно отслеживать статусы и делать refresh, но нельзя задать unsubscriber, fromResource — можно задать unsubscriber, но нельзя отслеживать статусы и делать refresh.
Так или иначе, можно добиться конечно, но ценой привнесения реализации хелпера со своей спецификацией. А если надо ослеживать статус и ошибку, придется усложнять интерфейс данных, добавляя status/error.
Сравните:
Если закрыть глаза на force (пока), то код вполне читабелен. В отличие от fromResource те же действия выражаются нативными кострукциями языка. Мне было интересно попытаться сделать на таком принципе.
Не то же самое, в MobX, детали, вроде value и status просачиваются в computed и в компоненты, сравните:
В lom_atom не надо обрабатывать случаи загрузки и ошибки в unfinishedTodoCount. Если в MobX забыть это сделать в unfinishedTodoCount, приложение сломается, что и видно в первом примере.
Не всегда апи бывает идеальным, например в сохранили todo через POST /todo, а после надо перевытянуть весь список /todos, т.к. другого способа получить обновленную сущность с серверным id нет.
Бывают случаи восстановления после ошибки, когда пользователь нажимает на кнопку Repeat и надо заново перевытянуть данные. В том же lazyObservable есть refresh().
Не хотел здесь это затрагивать. Тема для отдельной статьи, если сформулируете задачу, то могу на ее основе написать с примерами.
В том то и дело, автоматизировать обработку ошибок и статусов на mobx мне пока не удалось. Либо все упирается в большую сложность оберток поверх mobx. Либо уходом от mobx, небольшой доработкой базовой спецификации и гораздо меньшей сложностью реализации далее.