Pull to refresh

Comments 32

Я еще не писал конечных модулей, но все просто — подписываемся на обновления состояния и $forceUpdate.

А чем вам библиотека mobx-state-tree не угодила? Она сильно похожа на ваш стор

Хм, я в замешательстве. За долгое время поиска решения я не нашел ее.
Чувствую себя немного опустошенным теперь

Чую, что MobX для меня снова в приоритете.
Хотя, я бы еще проверил производительность на всякий случай, если эта либа связана с мобксом, то это небольшой оверхед
Блин, классная либа

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

Да, она решена. Чувствую себя еще более опустошенным))

Бывает, да и вы получили хороший опыт.
Жаль, что MobX так долго тянули с этим решением.

У редакса стор очень похоже выглядит с redux-orm

А зачем нужны отдельные секции computed, mutations? Не проще так:


class User extends EventEmitter {
    @observable name: string;
    @observable bestFriend: User;
    @observable additionalInfo: string;

    @computed
    get bestFriendsName() {
        return (this.bestFriend && this.bestFriend.name) || 'No best friend :C';
    }

    setName(newName: string) {
        this.name = newName;
    }
}

?

А разницы никакой и нет. Если записать то же самое через types.model, получится схожий код

Так зачем ещё одна библиотека? Да и разница всё же есть, для моего варианта даже без typescript будет работать автодополнение.

Автодополнение — задача IDE. У меня vscode вполне нормально дополняет.

Допустим, но вопрос зачем, так и остался открыт.

Здесь не знаю, но например в mobx-state-tree есть аналогичные actions. Их суть в группировке операций по изменению стейта. Таким образом можно регулировать атомарность записи в лог транзакций. Всякие undo/redo работают с группами базовых операций по изменению стейта.

Интересно было бы добиться такой же группировки на экшенах, с сохранением нативных классов, не наворачивая всяких фабрик поверх. Иными словами, как получить фишки редукса и mst без «very specific state architecture».
Интересно было бы добиться такой же группировки на экшенах, с сохранением нативных классов

декоратор action который будет заворачивать метод в транзакцию в конце которой будет делаться снимок изменений для undo/redo. Вроде легко должно реализовываться. Или я что-то забыл?

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

Пытаясь решить эти задачи, тащат и описание типов в run-time и описание ссылочности между ними и кучу всего, что уже сложно замаскировать под обычные объекты с декораторами.
Как, например в голом mobx узнать что изменилось в транзакции и получить патч изменений

не знаю как в mobx, у меня в cellx все изменённые в транзакции атомы попадают в releasePlan, каждый атом кроме _value имеет _fixedValue, который получит значение _value при завершении транзакции прямо перед генерацией события change, то есть прямо перед концом транзакции можно просто пройтись по releasePlan и запомнить эти два свойства. Если в mobx как-то так же, то проблем быть не должно, но если транзакции реализованы за счёт подавления вычисляемых, запоминания их и вычисления в конце транзакции, то да, вместо простейшего декоратора на 50-100 строк кода прийдётся писать хитрую библиотеку.


В более ранних версиях cellx были также транзакции в явном виде. В них как-раз в случае ошибки используется releasePlan для отката изменений банальным переписыванием _fixedValue в _value. То есть это настоящие атомарные транзакции и в случае ошибки где-то посреди транзакции все сделанные в ней изменения полностью откатывались. Mobx, насколько я знаю, всё ещё не умеет так, по крайней мере в чистом виде, без дополнительных библиотек.

Да уж, спасибо, не надо.

Когда смотрел ваш код, вспомнил observable из шарпа. Это было довольно удобно и необычно в то время, когда они появились. Но чтобы сделать нужное свойство observable, нужно было ему в set прописывать вызов ивента, который собственно оповещал об изменении. В итоге к каждой переменной добавлялся вызов метода NotifyPropertyChanged(Name). Такой подход мне как раз кажется этим лишним boilerplate из redux. Мне кажется библиотека должна брать на себя такие моменты, чтобы пользователь не писал каждый раз


setSomething(data) {
  this.mutate( ..., 'setSomething')
}

Возможно мне просто не повезло понять удобство такого подхода, но мне кажется если мы хотим подписаться на изменение параметра, мы можем сделать это проще.
Сам пользуюсь Vuex, computed и watch вполне пока удовлетворяют мои потребности.

Эту задачу решили в прикладных библиотеках .net. Например в EF это реализовано через proxy классы, которые на рантайме создаются.

Я о том же. В итоге появились magic либы. Не стоит проходить весь этот путь сначала.
Тоже свой велосипед писал, т.к. Redux мне сразу не понравился. Хотелось писать поменьше кода.
Если автору статьи или кому-нибудь еще интересно, то state management у меня выглядит так:

//Задается стор для хранения глобального состояния приложения. Также можно создать много отдельных сторов, а не один общий.
//Необязательно заранее инициализировать структуру вручную. Можно в любое время добавить любые данные в стор. Главное указывать правильный путь при обращении к нужному свойству.
const Store = new UIStore({
    booksList: [],
    selectedUser: {
        id: 1,
        name: "Yura",
        bestFriend: 1,
        additionalInfo: {
            age: 17
        }
    }
});

class AddtitionalUserInfo extends Component {
    componentWillMount() {
        //Подписка компонента на изменения стора.
        //Во втором  параметре передается локальный state компонента, если он нужен.
        //В третьем параметре передается массив путей к свойствам объектов в сторе, на изменение которых нужно подписаться.
        new UIState(this, null, Store, [{path: 'selectedUser.additionalInfo'}]);
    }

    componentWillUnmount() {
        this.uiState.removeState();
    }

    handleUpdateUserAge = (e) => {
        Store.update('selectedUser.additionalInfo.age', 20);
    };

    render() {
        return <div>
            <div>User age: {this.uiState.getStoreData('selectedUser.additionalInfo.age')}</div>
            <button onClick={this.handleUpdateUserAge}>Update user age</button>
        </div>
    }
}

Ваш пример на mobx будет выглядеть намного короче и удобней:


const Store = new class {
    @observable booksList = [];
    @observable selectedUser = new class {
      @observable id = 1;
      @observable name = "Yura",
      @observable bestFriend = 1,
      @observable additionalInfo = new class {
         @observable age = 17
      }
    }
}
@observer
class AddtitionalUserInfo extends Component {
    handleUpdateUserAge = (e) => {
        Store.selectedUser.additionalInfo.age = 20;
    };
    render() {
        return <div>
            <div>User age: {Store.selectedUser.additionalInfo.age}</div>
            <button onClick={this.handleUpdateUserAge}>Update user age</button>
        </div>
    }
}

1) Нет болерплейта и ручных подписок на стор в componentWillMount и componentWillUnmount
2) Нет неудобной записи с получением значения через строку this.uiState.getStoreData('selectedUser.additionalInfo.age') — мы можем прямо через стандартный js и точку обратится к любому свойству Store.selectedUser.additionalInfo.age
3) Нет неудобной записи с обновлением свойства Store.update('selectedUser.additionalInfo.age', 20); — разве не удобней записать обновление через свойства Store.selectedUser.additionalInfo.age = 20 а не возиться со строкой пути?
4) Как вы поступите если вам нужно будет в вашем сторе сохранять древовидные комментарии которые могут быть бесконечно вложенными? Если у вас стор можно считывать или обновлять только через указание пути к данным от корня то компонентам потребуется дополнительно еще передавать другу другу путь отдельным пропсом, когда же в mobx можно просто передать объект например <div>comment.children.map(comment=><Comment comment={comment}/></div> а потом в компоненте комментария обновить данные прямо на объекте this.props.comment.text = e.target.value без возни с путями, независимо от глубины в котором хранится этот комментарий.
5) Получение и обновление свойств через стоковый путь в сторе лишает возможности протипизировать работу с состоянием и отловить ошибки на стадии компиляции.


В mobx есть только один момент которого стоит придерживаться — нужно чтобы свойство на которое будут подписываться компоненты было помечено декоратором @observable
В примере, я записал вложенный объект через создание анонимного класса потому что декоратор можно объявить только в классе. В реальном приложении обычно синглтон-объектов очень мало а в процессе работы будут создаваться объекты с разным набором свойств и тогда логично и правильно эти типы вынести в отдельные классы. Например объект selectedUser будет не литералом а создаем объекта класса User а объект вложенный объект additionalInfo будет объектом другого класса Profile.


const Store = new class {
    @observable booksList = [];
    @observable selectedUser = new User({id: 1, name: "Yura", bestFriend: 1, profile: {age: 17}}) 
}
class User {
    @observable id;
    @observable name = "Yura",
    @observable bestFriend,
    @observable additionalInfo;
    constructor({id, name, bestFriend, profile}){
       this.id = id;
       this.name = name;
       this.bestFriend = bestFriend;
       this.profile = new Profile(profile);
    }
}
class Profile {
  @observable age;
  constructor({age}){
     this.age = age;
  }
}

Тут примерно полная аналогия с таблицами в реляционных базах данных — там нельзя создать вложенный объект в таблице и для этого нужно создавать отдельную таблицу. На клиенте точно также — таблицы просто будут классами — User, Profile, Post, Comment и т.д где мы объявляем типы колонок (для статической типизации) и помечаем декоратором @observable свойства которые рендерим в компонентах

1) Можно еще создать базовый компонент и в нем отписываться.
Это немного сократит код, но от подписки в componentWillMount у меня никуда не деться.

2-3) У меня можно тоже через стандартный js обратиться к любому свойству. Но как избежать ошибки, если свойства нет? Конечно, можно сделать проверку на существование каждого объекта перед конечным свойством. У меня же используется именно строка, чтобы рекурсивно проверить существование промежуточных объектов и конечного поля и, в случае их отсутствия, вернуть какое-нибудь дефолтное значение.

3) Ну да, стор у меня только при указании пути обновится.

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

5) По-моему, не лишает. У меня в стор можно передать не только значения отдельных полей, но и объекты. Правда методы объектов не сохраняться.

В mobx есть только один момент которого стоит придерживаться.
Я не так давно писал, скольких моментов там стоит придерживаться. И там не один.
Возможно, со временем привыкаешь, но по началу сложно что-нибудь не забыть.

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

Добавлю свои 5 копеек.

1. От connect и подписки componentWillMount в коде приложения можно избавиться, переопределив createElement, который будет создавать обертку с подписками вокруг исходного компонента
  /** @jsx lom_h */
  window['lom_h'] = lomCreateElement
Суть в автоматизации, биндинги за сцену выносятся. Все компоненты можно чистыми оставить.

2-3. Избежать ошибки, если свойства нет, а также индикатор загрузки приделать, можно обработчиком try/catch в render обертки
class Wrapped extends React.Component {
  render() {
    try {
       return this._origComponent(this.props, this.context)
    } catch(error) {
       if (error instanceof Wait) return this._loadingComponent({error})
       return this._errorComponent({error})
    }
  }
}


4. Весь объект лучше не передавать в пропсы ради оптимизации, можно попробовать развить идею контекстов.
function HelloView(_, store) {
   return <div>
        <input
            value={store.message}
            onInput={({target}) => { store.message = target.value }}
        />
        <br/>{store.message}
    </div>
}
HelloView.deps = [Store]
Обертка createElement по deps найдет Store, проинжектит в контекст. Идея в том, что если вам надо прокидывать до HelloView сторы транзитом сквозь кучу компонент, просто перенесите его в контекст. Т.е. контексты в приоритете над пропсами. Если надо переопеделить Store, то можно использовать что-то вроде:
const ClonedHelloView = clone(HelloView, [
  [Store, MyStore]
])

actions в MobX — нужен из-за оптимизации синхронной природы обновлений, в решениях с асинхронными обновлениями можно и без них. Однако actions позволяет лучше генерировать лог изменений стейта, когда понятно кто изменил состояние, это важнее. Также undo/redo можно построить на них.
Про наблюдаемый объект parentObj.level1Obj = observable({level2Obj: {propA: 10}}); вам ответили, что не обязательно в данном случае заворачивать в observable.

Для демонстрации работы с контекстами и обработки ошибок приведу пример на reactive-di.
Интересные у вас решения.
Но вот обертка с try catch проблему не решит. В компонентах все-равно потребуются проверки на существования объектов, иначе каждый второй компонент будет отображать сообщение об ошибке.
А какая ситуация? Зачем проверки могут потребоваться?

Если нет объекта, который надо отобразить — это исключительная ситуация, зачем что-то проверять?
Да, для optional-свойств мое решение не поможет.

Я не знаю ваших реальных задач, но по-моему все-таки лучше избегать optional.

Можно словари использовать, нормализовывать данные сразу, иначе где-нибудь все равно боком вылезет. Со строгой типизацией хотя бы flow/ts подскажут.
Sign up to leave a comment.

Articles