Search code examples
javascriptreactjsrefluxjs

Removing item in nested state results in incorrect item being removed in DOM


I'm working on a project that utilizes React and ReFlux. Currently I have just one ReFlux store: SubscriptionStore.js. It contains simple details around a user’s subscriptions, including a nested array of objects with details around each “Title” attached to their subscription.

Here is a simple breakdown of the data behind SubscriptionStore.js:

{
    hasThisSubcription: 0
    hasThatSubscription: 1
    nextPeriodStart: "2015-10-12"
    nextYearStartDate: "2016-09-12"
    accountTitles: [{
        id: "1"
        title: "The Name"
        createdDate: "2015-09-12 16:17:08"
    }, {
        id: "2"
        title: "Another Name"
        createdDate: "2015-09-12 16:17:08"
    }, {
        id: "3"
        title: "Yet Another Name"
        createdDate: "2015-09-12 16:17:08"
    }]
}

I've got a React component that allows the user to update the name of existing Titles, add new Titles, or delete Titles. Adding and renaming is going well, but removing is misbehaving and I'm sure it's because I don't yet have a full grasp on React/ReFlux.

Here is the relevant code from this main component:

var React = require('react')
var Reflux = require('reflux')
var SubscriptionActions = require('../../stores/SubscriptionActions.js')
var SubscriptionStore = require('../../stores/SubscriptionStore.js')

module.exports = React.createClass({

    mixins: [
        Reflux.connect(SubscriptionStore, 'subscriptions')
    ],

    /**
     * Add new title
     * @param  {number} id The Title ID
     */
    addTitle() {
        SubscriptionActions.addNewTitle()
    },

    /**
     * Remove Title from state using on ID
     * @param  {number} id The Title ID
     */
    removeTitle(id) {
        SubscriptionActions.removeNewTitle(id)
    },

    /**
     * Update Title title using ID
     * @param  {number} id  The Title ID
     * @param  {string} value  The new title
     */
    saveTitle(id, value) {
        SubscriptionActions.updateNewTitle(id, value)
    },

    render() {

        // Check for Title subscriptions and create an editable field for each
        var theTitles = []
        for (var i = 0; i < this.state.subscriptions.accountTitles.length; i++) {
            theTitles.push(
                <EditableCapsule
                    key = {i}
                    ref = {'title' + i}
                    removable = {i === 0 ? false : true}
                    labelValue = ''
                    clickToRemove = {this.removeTitle.bind(this, i)}
                    primaryAction = {this.saveTitle.bind(this, i)}
                />
            )
        }

        return (
            <div>
                <ScrollArea ref='scrollArea'>
                    {theTitles}
                </ScrollArea>
            </div>
        )
    }
})

And the relevant bit of code from SubscriptionStore.js:

removeTitle(id) {
    this.subscriptions.accountTitles = _.without(this.subscriptions.accountTitles, _.findWhere(this.subscriptions.accountTitles, { id: id }));
    this.trigger(this.subscriptions)
}

The Problem

Regardless of which Title I click on to remove, it's always the last title that gets removed. If I spit out a before and after of the this.subscriptions.accountTitles content it shows that the correct object is indeed being removed from the data, but React is rendering the elements as if it's always the last item being removed.

Any idea what might be happening?


Solution

  • don't use <EditableCapsule key = {i} ...: the whole point of key is to be a unique identifier for the exact element React has to work with for the purpose of updates, removals, etc.

    Its position in some array says absolutely nothing about the element itself, so: first change that to a proper key:

    ...
    
    render: function() {
      var accountTiles = this.state.subscriptions.accountTitles; 
      var tileset = accountTiles.map(this.buildTiles);
      return <ScrollArea ref='scrollArea'>{ tileset }</ScrollArea>;
    },
    
    buildTiles: function(tile, position) {
      var identifier = tile.title + title.id;
      return (
        <EditableCapsule
          key = { identifier }
          ref = { identifier }
          removable = { position !== 0 }
          labelValue = ''
          clickToRemove = { this.removeTitle.bind(this, tile) }
          primaryAction = { this.saveTitle.bind(this, tile) }
        />;
      );
    },
    ...
    

    Note that we're not removing based on array position now anymore, either. We'll be removing based on the tiles themselves. For obvious reasons.