0

I'm introducing myself to Vue.js (version 2.5.16) but am running into a problem understanding what looks like a problem with Reactivity:

I have two lists of components with v-for loops that render correctly most of the time - I can interact with the components and add new components. The components interact with a REST API representing 'zones', and whose function I'm hoping is rather obvious - It mostly returns JSON representations of either a single or all 'zones' for example:

{
    "state": "off",
    "pin": 24,
    "uri": "/zones/6",
    "name": "extra"
}

Whenever a zone is added or removed, the app reloads the entire list of zones. However, for some reason, when I delete a zone and trigger a reload of the zones, the list gets rendered with the wrong data! No matter which zone I delete, the last item of the list appears to be 'popped', rather than the expected zone not showing up on the new list! When I inspect the app.zones data, everything appears correct in the Javascript data but it just doesn't show up right in the browser.

Here's the relevant code:

...
<div class="tab-content">
    <div id="control" class="tab-pane fade in active">
        <ul>
            <zone-control
                v-for="zone in zones"
                v-bind:init-zone="zone"
                v-bind:key="zone.id">
            </zone-control>
        </ul>
    </div>
    <div id="setup" class="tab-pane fade">
        <table class="table table-hover">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Pin</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <tr is="zone-setup"
                    v-for="zone in zones"
                    v-bind:init-zone="zone"
                    v-bind:key="zone.id">
                </tr>
                <tr is="zone-add"></tr>
            </tbody>
        </table>
    </div>
</div>
...
<script>
    Vue.component('zone-control', {
        props: ['initZone'],
        template:
            `<li>
                <div class="btn btn-default" v-on:click="toggleState">
                    <span v-if="zone.state == 'on'" class="glyphicon glyphicon-ok-circle text-success"></span>
                    <span v-else class="glyphicon glyphicon-remove-sign text-danger"></span>
                    Zone: {{ zone.name }}
                </div>
            </li>`,
        data: function() {
            return {
                zone: this.initZone
            };
        },
        methods: {
            toggleState: function() {
                var state = (this.zone.state == 'on' ? 'off' : 'on');
                console.log('Turning zone ' + this.zone.name + ' ' + state);
                var comp = this
                fetch(
                    this.zone.uri,
                    {method: 'PUT',
                    headers: new Headers({
                        'Content-Type': 'application/json'
                    }),
                    body: JSON.stringify({state: state})
                }).then( function(result) {
                    return result.json()
                }).then( function(data) {
                    comp.zone = data;
                });
            }
        }
    })

    Vue.component('zone-setup', {
        props: ['initZone'],
        template:
            `<tr>
                <td>{{ zone.name }}</td>
                <td>{{ zone.pin }}</td>
                <td><div v-on:click="deleteZone" class="btn btn-danger btn-small"></div></td>
            </tr>`,
        data: function() {
            return {
                zone: this.initZone
            };
        },
        methods: {
            deleteZone: function() {
                fetch(
                    this.zone.uri,
                    {method: 'DELETE'}
                ).then( function(result) {
                    app.load_zones();
                });
            }
        }
    })

    Vue.component('zone-add', {
        template:
            `<tr>
                <td><input v-model="zone.name" type="text" class="form-control"></input></td>
                <td><input v-model="zone.pin" type="text" class="form-control"></input></td>
                <td><div v-on:click="addZone" class="btn btn-success btn-small"></div></td>
            </tr>`,
        data: function() {
            return {
                zone: {
                    name: '',
                    pin: ''
                }
            };
        },
        methods: {
            addZone: function() {
                console.log('Adding zone ' + this.zone.name);
                var comp = this
                fetch(
                    "/zones",
                    {
                        method: 'POST',
                        headers: new Headers({
                            'Content-Type': 'application/json'
                        }),
                        body: JSON.stringify({
                            name: comp.zone.name,
                            pin: comp.zone.pin})
                    }
                ).then( function(result) {
                    app.load_zones()
                    comp.zone = {}
                });
            }
        }
    })
    
    var app = new Vue({
        el: '#app',
        data: {
            zones: []
        },
        methods: {
            load_zones: function() {
                fetch("zones")
                .then(function(result){
                    return result.json()
                }).then(function(data){
                    app.zones = data;
                });
            }
        },
        created: function() {
            this.load_zones();
        }
    })
</script>

I've read through several references and there seem to be some edge cases or other 'gatchas' but this doesn't seem to fall into any of those. For example, this other question seems to agree that replacing the whole array is a pretty fool-proof way of altering arrays within the constraints of the Vue.js reactivity system.

Some other links to Vue.js documentation that I've checked and that are relevant but don't seem to point to my problem:

Reactivity in Depth

List Rendering

Common Beginner Gotchas

For the full code in context, see the github repo here.

UPDATE

Per the comments, I've reproduced the symptoms in a simplified JSBin. Click on any the button to see that, when the second element should be removed, the last one gets removed instead!

4
  • Have you tried: Vue.set(app, zones, data); rather than app.zones = data ? Commented Apr 19, 2018 at 2:53
  • Did now. I changed the line to Vue.set(app, 'zones', data);. No change in behavior. Commented Apr 19, 2018 at 2:59
  • you'll need to duplicate the problem in Plunker or JSBin or something. Then we can take a look. Commented Apr 19, 2018 at 3:01
  • Ok. I'll give that a shot... Commented Apr 19, 2018 at 3:06

1 Answer 1

0

Ok, I figured it out: It's all about the v-bind:key. I realized that I'm binding to an attribute that isn't provided by the API I'm fetching data from. Reference that to a (real and) unique attribute for each component and the problem goes away.

I did notice that the List Rendering Documentation mentions that

In 2.2.0+, when using v-for with a component, a key is now required.

Interestingly, the JSBin I linked above notably uses Vue.js version 2.0.3 but the problem still exists there and is fixed by proper use of the key attribute in the v-for loop.

If anyone else wants to see that, look at the JSBin linked in the question and add a v-bind:key="zone.id" to the <tr> element. The problem is resolved after that.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.