Back to the future with Proxy

We are programmers, the — dreamers. We follow the hype, we dream about the new silver bullet that can resolve all our problems. And we really like reinventing the bicycle which doesn’t really help us to overcome our obstacles but rather creates a few of new ones. Let’s dive into this little dream about architecture and how we are going to make a “new bicycle”.

Let’s try building the new architecture solution using modern approaches, considering experience of big brothers like Redux, Mobx, etc.

I remember the time when JS was an ugly language. All my colleagues were laughing at me; they thought that JS could only suit dropdown menu or simple animation. But now JS is a powerful language with a plenty of good things. Some of the ugly parts were completely rethought. More and more often we use patterns and solutions that allow us to build scalable large applications in an easier way.

Lets go!

As a disclaimer, in this article , we will make only state and save data, but we won’t make the View layer.

The future is here! We have ES2015+! It is a magic! But wait a minute… What can it give us? Will it help us to build our new super-duper architecture solution?

It is time to look at Proxy!

Proxy is a way of handling any operations with objects, such as set and delete. Currently proxy is compatible with all modern browsers except IE11. Vue3 framework will use proxy under the hood, and we will be using it too for our solution.

But using only Proxy is not enough. It only helps to track the interactions with objects. Let’s look at the other decent frameworks and methodologies for our idea.

Redux. Why do we like it? First, this is a one source of truth. All our logic is in one place. This is a big plus. Middlewares help implementing very different use cases, for example, — logging and debugging. That’s great, we will need middlewares too. But the main benefit in Redux is simplicity. We need to make our solution very simple.

Redux has drawbacks as well. For example, async manipulations, — we need to copy-paste a big parts of boilerplate code. We should evade this if possible.

Mobx is a great solution to track changes in objects and data. We will use Proxy instead. But Proxy works only with a flat object structure. We need to teach Proxy working with deep objects and arrays.

Type checker — it would be great to use type detectors and validate store properties.

RxJS — I like Rx because this guy can help me with pretty difficult use cases. Since we are targeted to maximum simplicity, we will use out of the box features of Rx.

React –is a critical framework. Before React we used Angular and , Ember. We wrote many directives and components with other abstractions, but React showed us the real component approach. Our applications are now more declarative, we can split them into small parts, and we use original HTML syntax with all JS benefits.

Declarative component approach is what we need. Explicit is better than implicit (the Zen of Python). Render-props are better than HOC. It is the modern thing, we can use it and it can make our code clear. It save our code in the one flow.

After analysis, we can start writing the code.

All modern frameworks are great, but for different specific cases. In this article we are resolving only state management problems and what is frameworks use that nothing else matters. I choose React because I know it better. But, ideally it needs to be universal.

We will make StateManager component. This is the main component, it saves all application state and updates all parts if it is needed. Something like Redux. If we need we can add validation or / and type checker (TypeScript). I prefer this solution for runtime validation — Yup

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
};

We can send all data and validation scheme in StateManager component, or we can send only a piece of data and send the rest later.

<StateManager
data={{
state: {}
}}>
{store => ...OUR APP... }
</StateManager>

The proxify method builds Proxy tree within deep data. It wraps all object and arrays who can mutate from _makeProxy method. In the Proxy I added 2 hooks: set, deleteProperty. These two hooks are enough for a start. If we set an object deep structure in Proxy. Proxy checkes it and if the latter is true, it makes proxify and wraps all child tree data. After that we are setting object and calling forceUpdate in this root component. ForceUpdate call reaction and all child nodes in our React application will be re-rendered with the new data.

The new state can be available in render-prop of our root component, and we can send it to all places in our application if it will be needed.

Proxy is an expensive way for handling the mutation. I analyzed performance in the mutation deep object with proxy and without proxy.

Without Proxy — 0.006ms

With Proxy — 0.05ms

This sample is without React or any other script, only clean HTML file in windows 7, the last version of Chrome. I hope it will be optimized.

With this simple approach what was implemented in a few hours, we can make to-do list with basic features, for example — add, remove, filter, and asynchronous get data.

<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>

In this article I wanted to share with you my idea of using Proxy and my previous experience of building new and better frameworks, approaches and tools. I hope it will inspire somebody to create a new bicycle in our JS Zoo. Good luck to all of us!

Links:

Live example To-do list