Chris_Alexander
Chris_Alexander

Reputation: 415

React + Firebase Component does not re-render on data change (remove)

This is a rendering / performance problem.

I've built a basic React App using Firebase. It basically works, with one notable anomalous behavior: rendering doesn't occur entirely on its own.

Specifically, I retrieve a list of groups to which a user belongs, and want to be able to add/remove groups, triggering a re-render. I'm using Firebase's .on('value'), .on('child_added'), and .on('child_removed') functions to bind references to these events.

Adding and removing groups works, however the group list disappears (especially noticed on remove), and I have to to click on a button (literally, any button in the UI) to bring it back (it pops up immediately, with all the correct data based on user action).

So - I'm clearly missing something somewhere, although I've been testing various fixes and have yet to be able to understand the issue. Components are:

Main Admin Component:

var React = require('react'),
    groupRef = new Firebase('http://my-url/groups'), //list of groups
    userRef = new Firebase('http://my-url/users'), //list of users
    groupListArray = [];

var AdminContainer = React.createClass({
    mixins: [ReactFireMixin],

    getInitialState: function() {
        return {
            groups: []
        }
    },
    buildGroupList: function (dataSnapshot) {
        groupListArray = [];
        var groupList = dataSnapshot.val();
        /* The group ids are saved as children in the db
         * under users/$userid/groups to reflect which groups a user can
         * access - here I get the group ids and then iterate over the 
         * actual groups to get their names and other associated data under
         * groups/<misc info>
         */
        for (var key in groupList) {
            if (groupList.hasOwnProperty(key)) {
                groupRef.child(key).once('value', function(snapShot2) {
                    groupListArray.push(snapShot2);
                });
            }
        }
        this.setState({
            groups: groupListArray
        });
    },
    componentWillMount: function() {
        userRef.child(auth.uid).child('groups').on('value', this.buildGroupList);
        userRef.child(auth.uid).child('groups').on('child_removed', this.buildGroupList);
        userRef.child(auth.uid).child('groups').on('child_added', this.buildGroupList);
    },
    handleRemoveGroup: function(groupKey){
        userRef.child(auth.uid).child('groups').child(groupKey).remove(); 
    },
    render: function() {
        <div id="groupAdminDiv">
            <table id="groupAdminTable">
                <thead>
                    <tr>
                         <th>Group Name</th>
                         <th>Action</th>
                    </tr>
                </thead>
                <GroupList groups={this.state.groups} remove={this.handleRemoveGroup} />
            </table>
        </div>
    }
});

module.exports = AdminContainer;

Then the GroupList Component:

var React = require('react');

var GroupList = React.createClass({
    render: function() {
        var listGroups = this.props.groups.map((group, index) => {
            if (group != null) {
                return (
                    <tr key={index} className="u-full-width">
                        <td>
                            {group.val().group_name}
                        </td>
                        <td>
                            <button className="button" onClick={this.props.remove.bind(null, group.key())}>Remove</button>
                        </td>
                    </tr>
                )
            } else {
                return (
                    <tr>
                        <td>You have not added any Groups.</td>
                    </tr>
                )
            }
        });
        return (
            <tbody>
                {listGroups}
            </tbody>
        )
    }
});

module.exports = GroupList;

Any help much appreciated!!

Upvotes: 1

Views: 2970

Answers (2)

Chris_Alexander
Chris_Alexander

Reputation: 415

I believe this was caused by use of .once(). As per Firebase's documentation, .once() is used to query a database location without attaching an enduring listener such as .on('value', callBack). However, when I changed the instances in my code where I was calling .once() to use .on() (even though I didn't want to attach a listener there but instead simply retrieve data one time), all the erratic behavior stopped and my app updated this.state and components as I had originally expected it to.

The only thing I can take away from this experience is that .once() does not function as intended / stated and that .on() (plus appropriate .off() references) should be used instead.

Upvotes: 1

Kyeotic
Kyeotic

Reputation: 19847

You appear to already understand what you need. In your componentWillMount method you register for various userRef events. You just need to register for the groupRef events, using the same callback. React re-renders whenever a components setState method is called, which you are doing inside buildGroupList. You just need to call buildGroupList after the groupRef updates.

componentWillMount: function() {
        var events = ['value', 'child_removed', 'child_added'];
        var refs = [groupRef, userRef.child(auth.uid).child('groups')];
        var callback = this.buildGroupList;
        refs.forEach(function(ref) {
            events.forEach(function(e) {
                ref.on(e, callback);
            });
        });
    },

Upvotes: 2

Related Questions