prescottprue / react-redux-firebase

Redux bindings for Firebase. Includes React Hooks and Higher Order Components.
https://react-redux-firebase.com
MIT License
2.55k stars 555 forks source link

feat(populate): nested populates #85

Open psych0der opened 7 years ago

psych0der commented 7 years ago

Hello there,

I have a data structure as follows

obj1 = {
   "Achildren": ["childobj1", "childobj2", "childobj3" ....],
   "Bchildren": ["childobj4", "childobj5", "childobj6" ....],
}

Achildren = {
   "childobj1": {
       "attr1": "val1",
       "attr2": "val2",
       "innerChildren": ["inchild1', ''inchild2', ''inchild3' .....]
    }    
}

InnerChildren = {
   inchild1: {...},
   inchild2: {...}
}

Is there a way that I can make nested populates work when I initially load Obj1?

prescottprue commented 7 years ago

There is not currently support for nested populates (they only work one level deep for now). Although this has been discussed, and I love the idea. I'll put it onto the roadmap.

prescottprue commented 7 years ago

Roadmap updated

psych0der commented 7 years ago

Thanks. Looking forward to use it soon :)

On Tue, Mar 21, 2017 at 1:48 PM Scott notifications@github.com wrote:

Roadmap http://react-redux-firebase.com/docs/roadmap.html#upcoming-minor-version-v140 updated

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/prescottprue/react-redux-firebase/issues/85#issuecomment-288006285, or mute the thread https://github.com/notifications/unsubscribe-auth/AA4q9aV9hKR2vL4Jnyujm_oDnjaUviGEks5rn4fygaJpZM4MiR8d .

-- Regards Mayank Bhola (+91-9953267725)

christianscheuer commented 7 years ago

@prescottprue is there also a way to expand keys instead of parameters? Say I have:

{
  "users": {
    "key1": {
      "name": "Adam",
      "licenses": {
        "licenseId1": true
      }
    },
    "key2": {
      "name": "Bob"
    }
  },
  "licenses": {
    "licenseId1": {
      "code": "xyz"
    }
  }
}

I want to populate the licenses index in my users with the full license objects from the /licenses. Since I'm not populating by a child with a fixed name, would that be possible with your code?

prescottprue commented 7 years ago

@christianscheuer It depends if you are talking about your own account or just the users list in general.

Users List - link to the example in the docs

const populates = [
  { child: 'licenses', root: 'licenses' }
]

@firebaseConnect([
  { path: '/users', populates }
])
@connect(({ firebase }) => ({
  users: dataToJS(firebase, '/users'), // users list not populated
  populatedUsers: populatedDataToJS(firebase, '/users', populates), // users with licenses populated
}))

Own Account - link to the example in the docs

const config = {
  userProfile: 'users',
  profileParamsToPopulate: [
   'licenses:licenses'
    // or object notation: { child: 'licenses', root: 'licenses' }
  ]
}

Since this isn't directly related to this issue, feel free to reach out over gitter or create another issue if you have more questions.

prescottprue commented 7 years ago

For clarity, here is the example I am using to work on this feature.

"todos": {
   $todoId: {
     "comments": {
       $commentId: true
     }
   },
},
"comments": {
  $commentId: {
    "owner": $uid
  }
},
"users": {
  $uid: {
    email: 'some@email.com',
    displayName: 'Some Guy'
  }
}

Populate a list of todos's comments, along with the "child population" of the owner of each comment. The connect syntax might look like so (could change):

const populates = [
  { child: 'comments', root: 'comments' }, // maybe childPopulates: 'owner' on here instead?
  { child: 'comments.owner', root: 'users' },
]

@firebaseConnect([
  { path: 'todos', populates }
])
@connect(({ firebase }) => ({
  todos: populate(firebase, 'todos', populates)
})

This feature is still in the works so syntax may change, but the hope is to get it into v2.0.0.

psych0der commented 7 years ago

The syntax looks exactly as expected 😃

prescottprue commented 7 years ago

@psych0der Good to hear.

The hope is to get this in to v2.0.0, but it was a but much to squeeze into v2.0.0-beta.

projoneftw commented 7 years ago

@prescottprue I see you're planning to add this to 1.6.0, isn't going into 2.0?

prescottprue commented 7 years ago

@projoneftw It will hopefully eventually make it into v1.6.0 as well v2.0.0 (since some still prefer the immutableJS toJS syntax).

The roadmap will indicate it under both, but switch the milestone back to v2.0.0 for clarity.

maitham commented 7 years ago

Hi Scott, awesome library! I think this is a vital feature. Is there an eta for when this is likely to make its way on to v2.0.0 ?

maitham commented 7 years ago

Is there a way we can achieve this now without waiting for populates method to be updated ?

prescottprue commented 7 years ago

@maitham1 Population can be done manually, populate is just a convenience method. That said, most agree that doing this by hand can get messy.

One Level Without populate

If you pass populates to your query, the queries needed to retrieve that data and place it in redux will be created.

Usage of populate in the connect function is up to you:

const populates = [{ child: 'owner', root: 'users' }]
@firebaseConnect([
  { path: 'todos', populates }
])
@connect(
  ({ firebase, firebase: { data: { users }, ordered: { todos } } }) => ({ // notice I grab ordered instead of data so it is an array
    todos: todos ? todos.map(todo => ({ ...todo, owner: users[todo.owner] })) : [],
    // equivalent to
   // todos: populate(firebase, 'todos', populates)
  })
)

Manual Nested Population

If you are trying to create queries for "nested" data, for now you will have to create queries for the needed data:

Continuing example from earlier nested population of owner inside of populated comments (or "nested"):

const populates = [{ child: 'owner', root: 'users' }]
const runNestedPopulate = (populatedTodos) => 

@firebaseConnect([
  { path: 'todos', populates },
  // NOTE: The following line will only be needed until nested population is supported
  { path: 'users' } // needed to get users queried on first level population 
])
@connect(
  ({ firebase: { data: { users, todos } } }) => {
    const todos = populate(firebase, 'todos', populates, [])
    return {
      todos: todos.map(todo => ({
         ...todo,
         comments: todo.comments.map(comment => ({
          ...comment,
         owner: users[comment.owner],
        })
      })
    }
  })
)

Future

As indicated the future hope would be to do the following:

const populates = [
  {
    child: 'comments',
    root: 'comments',
    childPopulates: [ { child: 'owner', root: 'users' } ]
  },
]

@firebaseConnect([
  { path: 'todos', populates }
])
@connect(({ firebase }) => ({
  todos: populate(firebase, 'todos', populates)
})

NOTE: The examples provided are not tested nor are they necessarily "suggested practice", and are provided to help discuss specifics.

maitham commented 7 years ago

@prescottprue Awesome thanks for this. I have a question isn't this method really inefficient, you're querying your entire user's directory, rather than the specific user you're after?

prescottprue commented 7 years ago

@maitham1 Yes it is inefficient to do, but will be more than fine for many applications. It can also help if you do type: 'once' on the users query.

This answer is part of why the future plan is to get nested population working (then only needed queries are run).

leesolway commented 6 years ago

Did this feature make it into a preview or anything? Is there a dev version that needs testing?