Joining Firebase tables in React

1,829 views
Skip to first unread message

Dave Dawson

unread,
Jan 6, 2016, 4:37:13 PM1/6/16
to Firebase Google Group

I am hoping to display a list of user's notes from a Firebase DB inside of a React app.


After reading through the Firebase recommended approach on structuring data, I've created my database in the flattened format they recommend. The data structure looks something like this:


    notes
    - [noteKey]
      - note: [noteData]
      - created_at: [date]
      - updated_at: [date]
    ...
    users
      - [userKey]
         - name: [userName]
         - notes
           - [noteKey]: true
           ... 
    ...


Each user has an array called notes, which lists the noteKeys of the notes that they own.


So far I've been able to get the full list of notes (from all users, not what I want), and the user's list of noteKeys. The issue that I'm having is combining those two. I have seen the question about joining tables, but I have more of a React focused question:


In which React function does the join happen?


Right now my code looks like this:


    getInitialState: function(){
      return {
        notesList: []
      };
    },
    componentWillMount: function() {

      base = Rebase.createClass('https://appName.firebaseio.com');
      base.syncState('notes', {
        context: this,
        state: 'notesList',
        asArray: true,
        queries: {
          limitToLast: 20
        }
      });

      this.state.notesList.map(function(item, i) {         
        base.child("notes/" + item['.key'] + "/note").on('child_added', function(snapshot) {
          console.log(item['.key'])
        }); 
      });
    },


I see two issues with this.

  1. 1. When the this.state.notesList.map function is called in componentWillMount, the array hasn't been populated with the Firebase data yet, so it looks like an empty array and returns an error.
  2. 2. Once I solve #1, I'm not sure how to get the user specific notes into it's own array that's accessible by the rest of the component.

--

  • In which React timeline function should the join be happening?
  • How do the second table items (the user's notes) get added to an array that is accessible by the rest of the component?

Jacob Wenger

unread,
Jan 7, 2016, 6:44:47 PM1/7/16
to fireba...@googlegroups.com
Hey there,

It looks like you are using Tyler McGinnis' re-base library. It may be useful to reach out to him on his GitHub repo to ask how he would handle this situation in his library.

That being said, I can guarantee that this.state.notesList is going to be undefined in your example since syncState() is an asynchronous API (it needs to go to Firebase's servers and back). It looks like the method is then()able, so you should move your mapping code into that.

As far as getting an individual note into an array, have you tried storing it in this.state via setState()? You can store arbitrary JSON in this.state and it will be available elsewhere in your component.

Cheers,
Jacob

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/846cec02-e282-4d12-9431-3e5281be4458%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Dave Dawson

unread,
Jan 14, 2016, 5:22:34 PM1/14/16
to Firebase Google Group
Thanks for this, Jacob! 

I'm definitely open to using ReactFire, and have actually been trying to get this working with that as well. Is that possible? The code that I have right now looks like this: 

    firebaseRef.child('users/' + authData.uid + '/notes').orderByChild('date_updated').on("child_added", function(noteKeySnapshot) {
        var ref = firebaseRef.child('notes/' + noteKeySnapshot.key());
        this.bindAsObject(ref, noteKeySnapshot.key()); 
    }.bind(this)); 

The issue with this solution is that each object that is returned ends up at the top of the `this.state` tree, instead of inside of an array (looks like this). Do you have a suggestion how how I can take these returned returned FB objects and add them into an array in state? It would be ideal to access them all from `this.state.notes`. 

Thanks!
Dave

Jacob Wenger

unread,
Jan 14, 2016, 5:43:50 PM1/14/16
to fireba...@googlegroups.com
Hey Dave,

Have you tried just using bindAsArray() on the parent? It should even handle queries and ordering for you!

var ref = firebaseRef.child('users/' + authData.uid + '/notes');
var query = ref.orderByChild('date_updated');
this.bindAsArray(query, "notes");

Cheers,
Jacob

Dave Dawson

unread,
Jan 14, 2016, 5:52:14 PM1/14/16
to Firebase Google Group
Thanks for the quick reply! 

The issue is that I'm attempting to use your suggested flat data approach. So my data structure looks like this: 

"notes" : {
    "n1" : {
        "note" : "[noteData]",
        "created_at" : "[date]",
        "updated_at" : "[date]",
    }
},
"users" : {
    "userOne" : {
        "name" : "[userName]",
        "notes" : {
            "n1" : true
        }
    }
}

So var ref = firebaseRef.child('users/' + authData.uid + '/notes'); only gives me the notekeys from the user, not the actual user. I'm trying to grab each notekey, find the matching Note in the Notes list, and add that object to an array. 

I'm still learning Firebase (obviously) and React, so if there's a simpler way of doing this and I'm missing something conceptually, I'm all ears. 

I also have a Stack Overflow question up about this too, in case that is helpful at all. 

Thanks!
Dave

Jacob Wenger

unread,
Jan 14, 2016, 6:15:27 PM1/14/16
to fireba...@googlegroups.com
Ahh sorry about that. That was a silly oversight on my part and that's what I get for not reading your initial question.

So, I think the answer depends a bit on what your use case is. For example, I think it's probably not a great idea to download all data for all notes every time a user loads your app. That can grow to a huge amount of data very quickly. It's probably best to show a list of notes which shows the date and title of each note. Ideally, you'd want to do that without downloading any of the notes' content. Once a user clicks on a note, you could then load just the data for that note. That will allow your initial page load to be fast and the subsequent loading of notes should be super quick as well since the websocket is already established and it won't be a ton of data.

To achieve this, you'll want to follow another common Firebase pattern: duplicating data. Keep a second copy of every note's title:
"users" : {
    "userOne" : {
        "name" : "[userName]",
        "notes" : {

            "n1" : <note_title>
        }
    }
Note that the only thing I changed was storing the note title instead of just true for each note. Now you can use bindAsArray() as I suggested to show your list of notes upon load. One you click on a note, you can use bindAsObject() to load just that note's data.

One issue you may run into is that you need to update the title of a note. This is a bit harder now, but can be solved pretty easily using multi-location writes.

Hope that helps!
Jacob

Dave Dawson

unread,
Jan 14, 2016, 6:22:03 PM1/14/16
to Firebase Google Group
That's a great suggestion. Thanks! A quick follow up: I would like to include a preview of the note text (the first 50 characters). Following along with this line of thinking, would the structure end up like this? 

"users" : { "userOne" : { "name" : "[userName]", "notes" : {
            "n1" : {
                "title" : <note_title>,
                "created_at" : <created_at>,
                "updated_at" : <updated_at>,
                "note_preview" : <note_preview>
        }
    }

In addition (and out of curiosity, because I've spent the last week trying to solve this problem), is there no way to use Reactfire or the Firebase web API to join two tables and display that data in an array in `this.state`? 

Jacob Wenger

unread,
Jan 14, 2016, 6:47:45 PM1/14/16
to fireba...@googlegroups.com
Yup, your thinking is good for showing the preview.

Note that while denormalization is a best practice in general, it is not always best for all use cases. For example, if you are a movie rating site, it doesn't make sense to copy all the movie information under each user who has reviewed that movie. Instead, you should just store the movie ID and store all the data in a /movies node. For your use case, every note probably only corresponds to a single user. So you don't have the same problem exactly. If you find that the data you are storing to show the preview is almost the same as the data to show the entire note, you might just want to avoid the denormalization and store all the data under the /users node.

Again, there are some rules of thumb which do not apply in all cases. One thing you want to ensure you can still do is, say, look up a user's profile info without having to load all their notes into memory. Say your data looked like this:

"users": {
  "$uid": {
    "name": <name>,
    "picture": <name>,
    "location": <name>,
    "notes": {
      "$noteId": {
        // ...
      }
    }
  }
}

In order to get the user's profile info (name, picture, location), I have to either do three on() listeners for each piece of data I want (that code is complex and repetitious) or I need to do an on() listener on /users/$uid (which will send down all the user's notes as well, which is a lot of extra data). It is far better to structure the data like this:

"users": {
  "$uid": {
    "profile": {
      "name": <name>,
      "picture": <name>,
      "location": <name>
    },
    "notes": {
      "$noteId": {
        // ...
      }
    }
  }
}

Now I can get all the profile data in one on() listener at /users/$uid/profile and not pull down any extra data.

I got a little off topic, but hopefully it gives you a better idea of the tradeoffs you have to make and how your data structure is important.

In addition (and out of curiosity, because I've spent the last week trying to solve this problem), is there no way to use Reactfire or the Firebase web API to join two tables and display that data in an array in `this.state`? 

It may seem odd at first, but arrays are not really a thing in Firebase / NoSQL databases. See here for some discussion of the topic. Yes, ReactFire has a helper method to turn a node into an array, but there are lots of caveats involved and the code for implementing arrays is non-trivial. There is almost always a comparable solution which doesn't use arrays and works better for realtime data.

Cheers,
Jacob

Dave Dawson

unread,
Jan 14, 2016, 7:17:29 PM1/14/16
to Firebase Google Group
For your use case, every note probably only corresponds to a single user. So you don't have the same problem exactly. If you find that the data you are storing to show the preview is almost the same as the data to show the entire note, you might just want to avoid the denormalization and store all the data under the /users node.

That totally makes sense. I may explore this direction. It seems much simpler. 

  if you are a movie rating site, it doesn't make sense to copy all the movie information under each user who has reviewed that movie. Instead, you should just store the movie ID and store all the data in a /movies node.

Of course. But in this example, how would you go about displaying the details of each Movie that a User has reviewed (or all of the Notes that a User has created) in React's `state` if Movies/Notes are a different object from Users?

For example:
Get all of a Users Note keys (ie. userID.noteKeys.[notekey]) {
   For each note key, find the corresponding Note object and return it (ie. Notes.[noteKey])
   Add the returned Note object into this.state.notes
}


...

Rob Koberg

unread,
Jan 14, 2016, 7:47:48 PM1/14/16
to Firebase Google Group
This is kind of what I am doing here: https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!topic/firebase-talk/zUsCxJswEyE
except that I am grabbing users associated with "ways" nodes which you could think of as notes.

The way it works in react, at least for me using a redux approach, is something (lifecycle event, ui event) triggers an action. In the example above I use componentDidMount because the data should be loaded into an initialState on the server. (I load the same function at server startup and refresh periodically). I call the action in componentDidMount. The action does some stuff and returns a promise in the payload that is sent to a reducer. The reducer gets a resolved promise and sets the necessary state properties. So the code looks like:
// src/browser/app/App.js React component
componentDidMount() {
const {actions: {onload}} = this.props;
onload();
}

// src/common/onload/actions.js
// get the onload function renamed to afterLoad here since UI gets rehydrated
import afterLoad from '../onload';

export const ONLOAD = 'ONLOAD';
export const ONLOAD_SUCCESS = 'ONLOAD_SUCCESS';
export const ONLOAD_ERROR = 'ONLOAD_ERROR';

export function onload() {
return ({firebase, settings: {s3BucketUrl}}) => {
console.log('onload actions s3BucketUrl', s3BucketUrl);
return ({
type: 'ONLOAD',
payload: {
promise: afterLoad(firebase),
},
});
};
}

// src/common/onload/reducer.js
import {Record} from 'immutable';
import {ONLOAD_ERROR} from './actions';

const InitialState = Record({
error: null
});
const initialState = new InitialState;

export default function onloadReducer(state = initialState, action) {
switch (action.type) {
case ONLOAD_ERROR: {
console.error('ONLOAD_ERROR action.payload.error', action.payload.error);
return state.set('error', action.payload.error);
}
}

return state;
}
Then there are reducers for the other reducers for the other top level objects, like inside of:
// src/common/users/reducer.js
export default function usersReducer({settings}) {
return function returnedUsersReducer(state = initialState, action) {
console.log('returnedUsersReducer action', action);
if (!(state instanceof InitialState)) {
const newState = revive(state, settings.s3BucketUrl);
return newState;
}

switch (action.type) {

case ONLOAD_SUCCESS: {
const users = setupData(action.payload.users, settings.s3BucketUrl, state.viewer);
return state.mergeDeep(users);
}
...

Rene Polo

unread,
Jan 27, 2016, 12:29:08 PM1/27/16
to Firebase Google Group
Thank you very much Jacob, reading this helped me a lot! 
Reply all
Reply to author
Forward
0 new messages