Comments 47
Звучит так как будто в mobx вам не хватало всего-то нескольких декораторов, а вы сделали полностью свою библиотеку
Еще обработка исключений построена иначе. Если начать рефакторить mobx, то каша из топора получится.
Да и весь алгоритм в 300 строк получается.
В MobX observable свойство — это обычно только данные. Mobx решает задачу как обновить состояние, что б точно перерендерилось необходимое.
Но не решает задачу, как абстрагировать компоненты от способа загрузки данных. Есть куча хелперов поверх mobx, которые пытаются это делать, но они не решают проблему бойлерплейта и инкапсуляции, компоненты все-равно знают о статусах.
В атомах, хэндер — часть спецификации ядра, на которую возложена задача абстракции канала связи, fetch. За сцену убираются детали, вроде pending/success/error.
В итоге получается значительно меньше шаблонного кода. Сравните 2 эквивалентных примера:
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}) {
return <div>
<ul>
{todoList.todos.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
</div>
}
class TodoList {
@observable todos = [];
@observable pending = false
@computed get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length;
}
@action fetchTodos(genError) {
this.pending = true
this.error = null
fetchSomeTodos(genError)
.then(todos => { this.todos = todos; this.pending = false })
.catch(error => { this.error = error; this.pending = false })
}
}
const TodoListView = observer(({todoList}) => {
return <div>
{todoList.pending ? 'Loading...' : null}
{todoList.error ? todoList.error.message : null}
<ul>
{todoList.todos.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {todoList.unfinishedTodoCount}
<br/><button onClick={() => {todoList.fetchTodos(true)}}>Fetch with error</button>
</div>
})
class TodoList {
@lazyFetch get todos() {
return fetchSomeTodos();
}
@computed get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length;
}
}
Осталось всего-то реализовать один декоратор, вместо написания новой библиотеки.
Я не нашел реализации lazyFetch, сложно судить. Но тут проблем больше, чем кажется на первый взгляд. Например, только некоторые из них:
А если надо запустить загрузку и отдать дефолтное значение?
Что будет, если в случае ошибки сперва будет запрошен unfinishedTodoCount?
Как заставить принудительно еще раз вытянуть значение с сервера?
Что будет, если у нас не промисы, а например Observable или вообще колбэки?
Как провернуть такую же штуку, только для сохранения данных, причем разделять запись в кэш и запись на сервер?
Что будет если где-то в observable или computed произошло исключение? Как нарисовать вместо сбойного компонента крестик, не руша все приложение?
Я не нашел реализации lazyFetch, сложно судить.
Ее я предлагаю написать. Ее в любом случае надо написать, независимо от используемой библиотеки, потому что эти танцы с @force
выглядят как "О_О что тут вообще происходит?!"
А если надо запустить загрузку и отдать дефолтное значение?
… то fromResource
из mobx-utils подходит идеально. Как раз под эту задачу создавался.
Что будет, если в случае ошибки сперва будет запрошен unfinishedTodoCount?
То же самое что и в lom_atom.
Как заставить принудительно еще раз вытянуть значение с сервера?
Тут уже из самой постановки задачи торчат уши чего-то ненормального.
Что будет, если у нас не промисы, а например Observable или вообще колбэки?
То все упрощается.
Как провернуть такую же штуку, только для сохранения данных, причем разделять запись в кэш и запись на сервер?
Вы тоже на этот вопрос ответа не дали.
Что будет если где-то в observable или computed произошло исключение? Как нарисовать вместо сбойного компонента крестик, не руша все приложение?
Так же как это делаете вы.
Ее в любом случае надо написать, независимо от используемой библиотеки, потому что эти танцы с force выглядят как «О_О что тут вообще происходит?!»Я преследую цель — придумать спецификацию, которая бы не зависела от внешних библиотек или хелперов, не усложняла бы интерфейс чистых классов. Force не самое удачное решение, но в этом смысле он менее всех захламляет предметную область. Может придумаю что-нибудь получше.
то fromResource из mobx-utils подходит идеально. Как раз под эту задачу создавался.Вообще мне не очень понятно разделение на lazyObservable и fromResource. Они частично копируют функциональность друг друга, при этом каждый что-то свое добавляет. Оба работают с даннымми в схожем стиле через sink, при этом в lazyObservable — можно отслеживать статусы и делать refresh, но нельзя задать unsubscriber, fromResource — можно задать unsubscriber, но нельзя отслеживать статусы и делать refresh.
Так или иначе, можно добиться конечно, но ценой привнесения реализации хелпера со своей спецификацией. А если надо ослеживать статус и ошибку, придется усложнять интерфейс данных, добавляя status/error.
Сравните:
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>
)
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 {
constructor() {
this.todoContainer = fromResource(
sink => sink(fromPromise(fetchSomeTodos()))
)
}
@computed get unfinishedTodoCount() {
return this.todoContainer.current().value.filter(todo => !todo.finished).length
}
}
const TodoListView = observer(({todoList}) => {
const todoContainer = todoList.todoContainer
const todos = todoContainer.current()
const unfinished = todoList.unfinishedTodoCount
return <div>
{todos && todos.state === 'fulfilled'
? <div>
<ul>
{todos.value.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {unfinished}
</div>
: <ErrorableView fetchResult={todos}/>
}
</div>
})
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>
}
В lom_atom не надо обрабатывать случаи загрузки и ошибки в unfinishedTodoCount. Если в MobX забыть это сделать в unfinishedTodoCount, приложение сломается, что и видно в первом примере.
Тут уже из самой постановки задачи торчат уши чего-то ненормального.Не всегда апи бывает идеальным, например в сохранили todo через POST /todo, а после надо перевытянуть весь список /todos, т.к. другого способа получить обновленную сущность с серверным id нет.
Бывают случаи восстановления после ошибки, когда пользователь нажимает на кнопку Repeat и надо заново перевытянуть данные. В том же lazyObservable есть refresh().
Вы тоже на этот вопрос ответа не дали.Не хотел здесь это затрагивать. Тема для отдельной статьи, если сформулируете задачу, то могу на ее основе написать с примерами.
Так же как это делаете вы.В том то и дело, автоматизировать обработку ошибок и статусов на mobx мне пока не удалось. Либо все упирается в большую сложность оберток поверх mobx. Либо уходом от mobx, небольшой доработкой базовой спецификации и гораздо меньшей сложностью реализации далее.
Кто вызывает destructor? Что произойдет если вызвать store.user.destructor()
а потом снова прочитать store.user
?
Не то же самое, в MobX, детали, вроде value и status просачиваются в computed
Это детали реализации fromPromise. Никто не запрещает вам сделать свой вариант который бы кидал исключение.
В lom_atom не надо обрабатывать случаи загрузки и ошибки в unfinishedTodoCount.
Потому что их обрабатывает какая-то магия на стороне вида. Добавляем такую же магию в mobx — и все работает.
В том же lazyObservable есть refresh().
Вот вы и нашли решение.
В том то и дело, автоматизировать обработку ошибок и статусов на mobx мне пока не удалось.
А в чем, собственно, проблема?
Если вызвать из приложения напрямую — будет плохо, закроется ресурс, это также как у результата 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 не было целей делать подобное в ядре, это библиотека просто работает с реактивным стейтом, не учитывая канал связи. Это неизбежно выливается в усложнение вышележащего уровня — хелперов. Усложнение как их реализации, так и их использования в прикладном коде.
Примитивность основы плохо сказывается на целостности экосистемы — появляется много решений, которые частично дублируют функциональность друг друга. Я исхожу из предположения, что кроме реактивности, абстракция работы с каналом связи — должна быть фундаментальным свойством фреймворка (а лучше языка). Эти вещи тесно связаны.
Как минимум, не получится сделать асинхронную неблокирующую загрузку, т.к. mobx не перехватывает внутри исключения
Кто вам это сказал?
С хорошей абстракцией, код TodoListView не должен меняться, если данные в todoList.todos были сперва захардкожены, а потом его решили получать с сервера.
Надо просто выдерживать одинаковый интерфейс. Механика зависимых свойств позволяет это делать.
Не будут работать некоторые оптимизации обновления состояния, например, в ситуации, когда мы записываем значение в свойство, а хэндлер его отвергает и возвращает свое, мы снова записываем первое значение, в этот момент не должно вызываться обновление компонента.
Будут. Работают.
Кроме вышесказанного могу добавить, что у mobx не было целей делать подобное в ядре, это библиотека просто работает с реактивным стейтом, не учитывая канал связи. Это неизбежно выливается в усложнение вышележащего уровня — хелперов. Усложнение как их реализации, так и их использования в прикладном коде.
Я перестал вас понимать.
Кто вам это сказал?В принципе можно через lazyObservable, но как это сделать без оберток над данными, не усложняя код?
Надо просто выдерживать одинаковый интерфейс. Механика зависимых свойств позволяет это делать.Вот слово «просто» бы раскрыть, какой это будет интерфейс? Если это интерфейс вроде {status, value, refresh}, предметная область будет замусорена такими обертками поверх данных.
Будут. Работают.Какая-то не конструктивная беседа получается, давайте конкретнее, может добавите пример какой-нибудь?
Как в mobx предложить значение? Мы его записываем в observable-свойство, но в реальности оно попадает в функцию, которая решает что с ним делать, вместо него записывает нормализованное значение и сохраняет на сервер.
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)
Вот, что я смог придумать на MobX и он вовсе не оптимизирует второе присвоение с тем же значением, called будет 3, а не 2 в конце. В случае сохранения на сервер, это означает что каждый раз будет вызываться тяжелая логика «save to server» с одним и тем же значением:
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. Если у вас есть понимание, как это сделать просто — попробуйте, всем будет интересно.
Зачем делать сохранение на сервер в перехватчике? Для этого вообще-то реакции существуют.
class Foo {
@observable data;
constructor() {
reaction(() => this.data, data => {
if (data !== undefined) {
//save to server
}
});
}
}
Ну или через autorun можно.
reaction не позволяет модифицировать записанное значение.
Кстати, а такой код утечки памяти не создает? reaction возвращает disposer.
Да пожалуйста:
class Foo {
private @observable _suggestedData;
private @observable _realData;
get data() { return this._realData; }
set data(value) { this._suggestedData = value; }
constructor() {
reaction(() => this._suggestedData, data => {
if (data !== undefined) {
//save to server
this._realData = { foo: [777] };
}
}, { compareStructural: true });
}
}
Нет, утечку такой код не вызовет. Disposer отписывает реакцию от тех observable на которые она подписалась — но если ни на эти observable ни на реакцию не осталось ссылок, то реакция будет и без всяких dispose собрана сборщиком мусора.
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)
На MobX:
let val = { foo : [777] }
let called = 1
class A {
@observable _suggestedData;
@observable _realData;
get foo() { return this._realData; }
set foo(value) { this._suggestedData = value; }
constructor() {
reaction(() => this._suggestedData, data => {
if (data !== undefined) {
called++
//save to server
this._realData = { foo: [777] };
}
}, { compareStructural: true });
}
}
// ...
a.foo = {foo: [666]}
assert(called, 2)
assert(a.foo.foo[0], 777)
a.foo = {foo: [777]}
assert(called, 3)
assert(a.foo.foo[0], 777)
fiddleassert(called, 3) вызовется 3й раз, если мы засетим значение {foo: [777]}, а по-логике не должен, т.к. это значение выставит хэндлер.
В общем то да, можно на mobx сделать и аналогичную логику. Только вот как это автоматизировать, что б не было шаблонного кода. Мне эта задача не кажется простой.
Идея mobx все же немного другая — это достаточно низкоуровневый конструктор с кучей хелперов, из которых можно гибко выстроить сложную кастомную логику.
Атомы — это больше соглашения, приняв которые, можно с минимумом бойлерплейта решать типовые задачи в веб без привнесения хелперов, без ручного программирования деталей таких вот крайних ситуаций.
Все же сложновато их сравнивать с аналогичной псевдо-оберткой над mobx.
но если ни на эти observable ни на реакцию не осталось ссылок
В общем случае вы не можете это гарантировать. Например, если где-то в глубине computed свойств кто-то подпишется на любое глобальное состояние (например, на размер экрана), то такая реакция будет жить вечно и постоянно выполнять бесполезную работу. Поэтому лучше выстраивать все computed-ы приложения (а реакция — тот же computed) в одно большое дерево, которое чётко определяет какие вычисления ещё кому-то нужны, а какие — уже нет
Ваш код можно было бы переписать куда короче:
class Foo {
@ $mol_mem
data( next : any , force? : $mol_atom_force ) {
const resource = $mol_http.resource( '/data' )
return resource.json( next , force )
}
}
Кода меньше, а делает больше:
- Загрузка данных с сервера с кешированием:
foo.data()
- Перезагрузка данных с сервера с обновлением кеша:
foo.data( undefined , $mol_atom_force_update )
- Сохранение данных на сервер с кешированием:
foo.data({ foo : [666] })
- Пересохранение данных на сервер с обновлением кеша:
foo.data( { foo : [666] } , $mol_atom_force_update )
Конкретно в этом случае — гарантировать это я могу. Потому что обращаюсь только к местным свойствам.
А в вашем коде я не вижу константы 777. Без нее код и на mobx был намного меньше.
Константа — это то, что возвращает сервер, а вернуть он может не то, что мы посылали. Так как вы реализуете 4 упомянутых мной сценария?
Нее, погодите. В примерах выше она устанавливалась синхронно, а не асинхронно. Она не может быть ответом сервера.
- В случае $mol_atom и lom_atom — может. Я привёл пример полностью рабочего кода.
- В случае MobX там будет асинхронная установка.
Если данные всегда проходят через сервер — тогда все еще проще.
Настолько проще, что код написать лень?
Да. Поясняю: исходная задача выглядела как задача с магическим двусторонним потоком данных к свойству, когда надо было перехватывать изменение значения и что-то хитрое было с ним делать.
Уточненная вами задача сводится к обычному одностороннему потоку данных, который все писать умеют.
Не сводится, прочитайте по внимательнее все 4 кейса использования. Для каждого из них вам придётся завести отдельный observable.
Ну и что? Если задача, требующая всех 4х кейсов, встречается регулярно — можно построить абстракцию.
Да почти всегда при работе с удалёнными запросами нужны все 4 кейса. Какую абстракцию вы построите?
Например, класс с 4 методами. По методу на предложенный вами кейс. И интерфейс к нему. И, если понадобится, стандартные обертки над этим интерфейсом.
Получится нормальный самодокументируемый код. Вместо этой магии с next : any , force? : $mol_atom_force
, значение которой без копания в мутных статьях невозможно даже понять.
На выходе, ежели мне понадобится такая абстракция, будет что-то вроде
class Foo {
data: IChannel<Data> = http.resource("/data").asJson
}
Примитивность основы плохо сказывается на целостности экосистемы
Наконец-то я нашел хорошо сформулированную причину ущербности JS :)
Кстати, почему у вас сравниваемые виды имеют разную функциональность? Какой смысл в таком сравнении?
Если уж совсем быть точным, то вот совсем эквивалентный пример на MobX и lazyObservable, с ленивой загрузкой и абстракцией от способа получения данных.
class TodoList {
constructor() {
this.todoContainer = lazyObservable(sink => sink(fromPromise(fetchSomeTodos())))
}
@computed get unfinishedTodoCount() {
const todos = this.todoContainer.current()
return todos && todos.status === 'fulfilled'
? todos.filter(todo => !todo.finished).length
: []
}
}
const TodoListView = observer(({todoList}) => {
const todoContainer = todoList.todoContainer
const todos = todoContainer.current()
return <div>
{todos && todos.state === 'fulfilled'
? <div>
<ul>
{todos.value.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {todoList.unfinishedTodoCount}
</div>
: <ErrorableView fetchResult={todos}/>
}
</div>
})
кроме fetch, но над ним можно сделать обертку, позволяющую записать его в псевдосинхронном виде
Лучше именно так и делать, чтобы происходила автоматическая отмена запросов, при уходе на другую страницу. Кроме того она позволила бы делать 1 запрос вместо 2, если на странице расположить два туду-листа.
Условно можно сказать, что тут эффекты внутри самого свойства. По сути, это спецификация, позволяющая с минимальным бойлерплейтом описывать observable-свойства с эффектами и управлять этими эффектами. Накрутить в них тротлинг не проблема, также как и сделать отмену. Вообще отмена автоматически происходит, когда компоненты больше эти данные не выводят.
Зачем диспатчить экшен, когда можно просто изменить свойство в объекте, если конечно вам не нужно версионирование и фишки, вроде time travel. Экшены можно накручивать поверх, как например в mobx-state-tree.
Лучше накидайте пример на псевдофреймворке, как вы себе это представляете, а я попробую накидать аналогичный, на lom.
Зачем городить эффекты, экшены, реакции, выстреливать store.fetchMessage() в componentDidMount, если достаточно просто обратиться к свойству store.message, в котором сработает логика обновления этого свойства.
Под объект мы маскируем не только реактивность, но и канал связи. Это сильно связанные задачи, код упрощается за счет автоматизации типовых задач.
Вот пример, который асинхронно загружает данные, обрабатывает ошибки и рисует статус загрузки:
// ...
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'));
Кстати, lom_atom не навязывает такой подход, его можно использовать как обычный стор:
// ...
function fetchMessage() {
return new Promise(resolve => {
setTimeout(() => resolve('message'), 1500)
})
}
class Store {
@mem message: string = ''
fetchData() {
fetchMessage()
.then(message => { this.message = message })
.catch(error => { this.message = error })
this.message = new mem.Wait()
}
}
function HelloView({store}) {
return <div>
<input
value={store.message}
onChange={({target}) => { store.message = target.value }}
/>
<br/>{store.message}
</div>
}
const store = new Store();
store.fetchData()
ReactDOM.render(<HelloView store={store} />, document.getElementById('mount'));
Надеюсь, я смог показать на примере атомов, как небольшая доработка базовой концепции может существенно упростить код в типовых задачах для веба и избавить компоненты от знания деталей получения данных.
Всегда думал, что упростить код можно, сократив его количество. В вашем случае упрощение выглядит как добавление новых декораторов и еще какой-то своей логики. Глядя на код совсем не очевидно как все работает. Почему нельзя загрузить данные в стор независимо от компонентов и брать эти данные в тех местах где они нужны? С чем связано это загадочное правило что компонент не должен знать откуда берутся данные и вообще он должен быть тупым по определению? Раз уж тупой, то и любая логика внутри него не должна существовать.
pending(я использую
loading) можно хранить переменную состояния loaded. Ну и делать соответствующие проверки в компоненте.
Еще, на всякий случай, подчеркну что стоит использовать componentDidMount для сайд эффектов
Например, нарушение инкапсуляции — данные сегодня были захардкожены, а завтра их решили получать с сервера (список юридической отвественности, например АО, ИП и т.д.). Факт того, что данные асинхронны, протекает в компоненты в виде вызовов fetchSome в componentDidMount и в виде структур данных {status, value, refresh}. Т.к. заранее предугадать это было нельзя, то надо рефачить компонент, который мог использоваться в 10 проектах, соотвественно они будут затронуты со всеми вытекающими.
Дублирование кода — у вас одни и те же данные могут быть в разных компонентах использованы, получается в каждом надо вызывать componentDidMount. Иначе нет гарантии, что данные актуальны.
Нарушение единственной ответственности (SRP). Основная задача компонента — генерить верстку. Но реактовый компонент в виде класса — это все вместе: стейт, логика и верстка, т.е. у него много отвественностей. В небольших масштабах это удобно — просто и все под рукой. Однако с ростом кодовой базы много проблем возникает в поддержке и кастомизации, как в вышеописанном примере.
Работа с данными и сервером — зона ответственности слоя данных, моделей, например. Неважно кто запросил данные, сам факт запроса означает, что данные надо вытянуть с сервера, если они не актуальны. Правильное распределение отвественности позволяет избавиться от дублирования кода за счет автоматизации. Актуализация становится автоматической, код упрощается.
В свете такого разделения отвественности, компонент реакта должен быть только тупой. Во всяких Vue, Angular-ах, WebComponents, верстка — отдельная сущность. При проектировании реакта не заглядывали так далеко, он был нужен для быстрой разработки небольших приложений и с этой задачей хорошо справляется, хоть и не соблюдает все эти мудреные принципы.
Ну и делать соответствующие проверки в компоненте.Присмотритесь к примерам в статье на lom_atom, там в компонентах проверок нет вовсе, как будто нет асинхронных запросов к серверу. При этом статусы и ошибки обрабатываются.
- нет места в приложении, отделённого от компонента, где надо инициировать загрузку (как в примере с ридакс-сагой), описание необходимых данных указываются декларативно только в том компоненте, где они нужны
- нет нарушения ленивости загрузки; не надо заботиться, как и когда данные будут загружены
- мутации дают простую обработку, когда данные ещё не пришли, обработку ошибок и статусов
А вот этого:
У такого подхода правда есть и минус — необходимо сперва вытащить из todoList todos и users и только потом с ними работать, иначе будет последовательная загрузка и оптимизации не получится.у Relay нет.
Ну и беглый поиск показывает, что и тут тоже не без проблем.
Relay, Apollo — достаточно тяжелые решения. Кроме того, что могут атомы, они дают optimistic updates, проверку целостности по схеме, сервер. Но если эти навороты не нужны, кода будет значительно меньше.
Т.к. атомы — легковесная абстракция между fetch и компонентами, то вместо fetch вполне может быть какой-нибудь graphql-js или apollo-client. Тут как раз больше возможностей для масштабирования.
На мой взгляд, логично переложить часть функциональности из Relay-подобных решений, в mobx-подобные решения. Общеизвестных библиотек для работы с состоянием все-таки меньше чем фреймворков для рендеринга. Вместо биндингов к реакту и прочим, можно было бы сделать пару биндингов от graphql к этим библиотекам.
Длинные уши асинхронности