
Мы, программисты — мечтатели. Идем на поводу у хайпа, мечтая о новой серебряной пуле, которая решит все наши проблемы. А также, мы любим писать новые велосипеды, тем самым не решая проблемы, а создавая новые. Давайте в этой статье немного помечтаем об архитектуре, разрабатывая «Псевдо-новый» велосипед.
Попробуем разработать свое архитектурное решение, используя современные подходы, с оглядкой на старших братьев Redux, Mobx и т.д.
Находясь в замечательном времени, в котором никому ранее не нужный JS, способный всего лишь анимировать DropDown, стал неожиданно мощнейшим языком! С кучей интересных, новых возможностей и переосмысленных «старых» особенностей. Все чаще и чаще, мы не идем в лоб, а проектируем нашу архитектуру, применяем патеры и решения, способные облегчить разработку и создать масштабируемый и легко усвояемый код.
Итак, начнем!

Сразу оговорюсь, что в нашем велосипеде мы будем решать вопрос не отрисовки View, а хранения данных и реакций на их изменения.
Будущее уже наступило! У нас есть ES2015+! С ним мы можем творить чудеса! Но, постойте… А что он нам дает? Чтоб вот так взять и написать наш новый супер-велосипед, который непременно должен решить все наши проблемы?
Давайте посмотрим на Proxy! Proxy, это способ отслеживать операции над объектом, такие как удаление, изменение и прочее. Сейчас Proxy поддерживается везде, кроме IE11, Vue3 под капотом будет использовать ее же. Ну а чем мы хуже, нечего плодить legacy, пусть IE11 останется за бортом нашей галеры, мы поплывем в светлое будущее. Короче, для велосипедостроения первый ингредиент будет — Proxy.
Но Proxy не решает всех проблем, да и не должен решать, всего лишь способ отследить взаимодействия с объектом. Давайте посмотрим на лучшие практики наших любимых решений.

Redux, что мы в нем любим? Единое место хранение данных для нашего приложения, способно хендлить изменения в одном месте, не размазывая логику по всем частям. Это плюс, берем! Middlewares, они способны расширять и дополнять функционал, организовать логирование, проксирование и т.д. Замечательно! Но самый главный плюс Redux’a, как по мне, это простота. Вот и нам, в нашем велосипеде, надо сделать все максимально просто.
Из минусов, это работа с асинхронностью. Много лишнего кода, который приходится писать… Надо как-то этого избежать.
Mobx — отличное решение, позволяющее отслеживать изменения. Этим у нас будет заниматься Proxy. Единственное, в дальнейшем, научим Proxy работать со вложенными структурами.
Типизация. Было бы неплохо использовать эту популярную нынче тему и даже дополнить её. Сделаем в нашем супер решении типизацию и валидацию в рантайме, с учетом вложенности.
RxJS, люблю его за то, что во многих случаях он способен за меня решать сложнейшие задачи, но нам не зачем его имплементить. Постараемся не усложнять наше решение, а использовать обычные объекты JS и возможно Rx будет работать с нашим велосипедом как нужно.
React, здесь я бы хотел остановиться поподробнее. React, это переломный фреймверк. До React’a у нас был Ember и Angular 1, дающий первичное представление о компонентом подходе. Мы писали много компонент и директив, дополняя основную логику, которая была сосредоточена в контроллерах, сервисах и т.д. Но с появлением React’a, мы стали рассматривать наше приложение более декларативно, не убегая от HTML, в том же контексте и можно сказать синтаксисе.
Декларативность компонентного подхода, это то что нам нужно достичь, чтобы все было максимально прозрачно и понятно. Модные сейчас render-props дают отличную возможность не выбиваться из потока и писать логику еще более декларативно, чем HOC’и, так что и их возьмем.
После того, как мы примерно определили, как это все должно выглядеть, давайте начнем писать код

Каждый из современных фреймверков хорош по-своему. Мы сейчас решаем проблему хранения состояния, по сути мы должны его использовать где угодно. В данной статье, для примера, я выбрал React, так как лучше его знаю. Но в идеале, это должно быть универсально.
Создадим компонент StateManager. Это основной компонент, хранящий все состояния приложения, как Redux. По желанию мы можем добавить валидацию (например, это решение) и типизацию (TypeScript/Flow). А также, написать по желанию функционал Middlewares, которые будут отрабатывать изменения до, после, во время (поместив их вызов в Proxy).
компонент StateManager
class StateManager extends Component { componentWillMount() { this.data = this.props.data || {}; this.proxify({props: this, val: 'data'}); } proxify = ({props, val}) => { Object.keys(props[val]) .filter(prop => typeof props[val][prop] === 'object' || Array.isArray(props[val][prop])) .map(prop => ({props: props[val], val: prop})) .forEach(item => this.proxify(item)); props[val] = this._makeProxy(props[val]); }; _makeProxy = props => { return new Proxy(props, { set: (target, key, args) => { if (typeof args === 'object' || Array.isArray(args)) { target[key] = args; this.proxify({props: target, val: key}); } else { target[key] = args; } setTimeout(this.forceUpdate.bind(this)); return true; }, deleteProperty: (oTarget, key) => { setTimeout(this.forceUpdate.bind(this)); return true; } }); }; render() { return this.props.children(this.data); } } StateManager.propTypes = { data: PropTypes.object.isRequired };
По порядку, что тут происходит. Извне, мы пробрасываем данные нашего приложения, либо пустой объект (схему), который будем заполнять в процессе. Заворачиваем наше приложение
Использование StateManager
<StateManager data={{ state: {} }}> {store => ...НАШЕ ПРИЛОЖЕНИЕ… } </StateManager>
метод proxify рекурсивно трансформирует в Proxy все объекты / массивы, делая их восприимчивыми на изменения, через метод _makeProxy.
В самом прокси объекте я добавил 2 хука — set, на любое изменение, deleteProperty — на любое удаление. В set мы смотрим, если у нас пришел объект, массив, или глубокие данные, мы их снова прогоняем через proxify, после чего вызываем forceUpdate, что приведет к перерисовке всех дочерних элементов с обновлённым состоянием нашего прило��ения.
Пробрасывая измененное состояние через render-prop, мы автоматически сможем его разбросать в любые части приложения.
Так как, по сути, это обычный объект, мы можем к нему прикрутить тот же Rx, или что угодно, мутировать когда угодно, и вообще извращаться сколько влезет.
По скорости Проксируемый объект существенно отличается от обычного присваивания. Провел замеры на создание и присваивание строки в объект с глубиной вложенности 2:
для обычного объекта
~ 0.006ms
Proxy
~ 0.05ms
Это обычное присваивание без реакта и прочего. Будем надеяться, что это оптимизируют.
С этим примитивным подходом, написанным за несколько часов «на коленке», можно создать такой to-do list, в котором данные приходят асинхронно, и мы можем управлять выводом элементов
To-do list
<StateManager data={{ state: {} }}> {store => <div> <button onClick={() => store.state = { todo: { items: [ {name: 'Get data', state: 'in-progress'}, {name: 'Bind proxy to all', state: 'in-progress'}, {name: 'Force update root component', state: 'in-progress'}, {name: 'Update data in children', state: 'in-progress'} ], filter: '' }} }>Load todolist</button> <hr/> <div> { store.state.todo && store.state.todo.items && [ <ul key="todolist"> { store.state.todo.items .map((item, index) => <li hidden={!(store.state.todo.filter === '' || item.state === store.state.todo.filter)} style={item.state === 'done' ? { textDecoration: 'line-through' } : {}} key={index}> {item.name} {item.state === 'done' ? <button onClick={() => store.state.todo.items[index].state = 'in-progress'}>Set to in progress</button> : <button onClick={() => store.state.todo.items[index].state = 'done'}>Set to done</button>} <button onClick={() => store.state.todo.items.splice(index, 1)}>Remove</button> </li> ) } </ul>, <div key="add-new"> <input type="text" ref={c => this.input = c} /> <button onClick={() => { if (this.input && !!this.input.value) { store.state.todo.items.push({name: this.input.value, state: 'in-progress'}); this.input.value = ''; } }}>Add new item</button> </div>, <select key="filter" onChange={e => store.state.todo.filter = e.target.value}> <option key="all" value="">All</option> <option key="in-progress" value="in-progress">In progress</option> <option key="done" value="done">Done</option> </select> ] } </div> <br/> <br/> <br/> <hr/> <h3>State:</h3> <pre>{JSON.stringify(store, true , 2)}</pre> </div> } </StateManager>

Данным примером я хотел поделиться с вами своими мыслями о использовании Proxy и нескольких хороших практик. Надеюсь, что использование новых стандартов вдохновит Вас на создание полноценных решений, которые достойно займут свое место в нашем зоопарке JS. Всем спасибо, удачи в велосипедостроении!
Ссылки:
→ Исходный код
→ To-do list поклацать
