Aurelia-store and observable properties: a cautionary tale

I’m posting this in case anyone else runs into this little snag.

While experimenting with aurelia-store, I decided I’d like to avoid making “temporary” properties on my view-model for the purpose of binding to form data. Instead, I wanted to bind my user input directly to the properties plucked from the state, and furthermore make them @observable, so I could easily dispatch a state update whenever any of them changed. (Is this a good idea? I don’t know; someone please weigh in if you have an opinion.)

import stuff

@autoinject
@connectTo({
    selector: (store) => store.state.pipe(pluck("myProperty")),
    target: "myProperty"
})
export class MyVM {
    @observable myProperty;        // OK - this will hold the latest plucked value, *and* be observable
    myPropertyChanged(newVal, oldVal){
        this.store.dispatch("updateMyProperty", newVal);
    }

    constructor(store){
        store.registerAction("updateMyProperty", (state, newVal) => return Object.assign({},state, {myProperty: newVal});
    }
}
<template>
    <form><input type="text" value.bind="myProperty"></input></form>
</template>

Now assuming my syntax is correct (I haven’t tested the exact code above), this should pipe the value of store.state.myProperty into MyVM.myProperty; and any changes to MyVM.myProperty should dispatch an update to the store. And conveniently, @observable properties only call their change handler if newVal is different from oldVal, so we shouldn’t get stuck in an endless update loop that crashes our browser.

…right?

But that code will crash your browser!

Luckily, the reason and the solution are very simple: the @observable decorator and the @connectTo decorator both look for a change handler, and they both expect it to be named according to the same convention: [property-name]Changed(newVal, oldVal). Writing a single change handler that accidentally fits both those slots is very easy to do, and it causes some wild behaviour.

Solution: just use the syntax @observable({changeHandler: 'someOtherFunctionName'}) to define your observable change handler, and you’re all set. You can connectTo, pluck, observe/dispatch, and bind using a single property name, and it all works seamlessly.

3 Likes

In general you should try to avoid to manipulate the state by any means other than dispatching a new action. Using two-way bindings directly on a state slice will do pretty much exactly that. If you’re writing a history aware app you’ll pretty soon recognize that your previous states have been modified without the store noticing it.

I’ve updated the docs with a new section to explain how to avoid it

2 Likes

Thanks! Yes, after giving your post some thought I see this is a bad design choice.

It works when only this component is modifying the local state, but if there were another component trying to send state update through the same observable, it would get muddy – we lose the concept of “current state”, since a single property is expected to do double duty representing the original state and the modified state.

2 Likes

Two-way binding and Aurelia Store is a recipe for a bad time. If I get the time, I would love to have a stab at creating a forms plugin for Aurelia Store which would bypass the issues of directly mutating state from a subscription. I am not sure of the design of such a plugin, but I know Redux has a plugin for this called Redux Form to address the same problem of pass-by-reference mutation on source values.