React-redux-firebase: multiple firebaseConnect calls

Created on 13 Nov 2017  Â·  12Comments  Â·  Source: prescottprue/react-redux-firebase

Do you want to request a feature or report a bug?

First, I’m really glad for the existence of this tool. It is a really great idea. I’m not a professional with it yet. I’m currently building my second project using it. I’m rather confident that this is a bug. It actually might be two bugs to be perfectly honest.

What is the current behavior?

It would seem that firebaseConnect only works on the first render of the app (once). This problem is manifest when not all components are rendered immediately and also when not all props exist immediately. For example the following code will never load the path ‘path1’ if it is not a hard-coded prop. (using flow notation):

const composedComponent = compose(
  connect(mapStateToProps, mapDispatchToProps),
  firebaseConnect((ownProps: OwnProps): Array<string> => {
    if (ownProps.myProp) {
      return ['/path0', '/path1']
    } else {
      return ['/path0']
    }
  })
)(myComponent)

Note that the first branch of the logic is in fact taken when the prop becomes available but the state is never loaded. If the code is modified to always take the first branch then it works fine of course.

I’ve also seen had difficulty when using child components that are called after the initial firebaseConnect call in a parent. Much like the above case, appropriate branches will be taken but because they occur after the first connection request, the state is never loaded. In my case, I attempted to switch into a module which used the ‘firebaseConnect’ method from a parent-module which was also using the ‘firebaseConnect’ notation itself.

Because most projects involve accessing resources at database paths not yet resolved, I’m left to assume one of two things: either this is a bug or there exists some sort of reconnect method that I’m not aware of.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via codesandbox or similar.

If you need more information (and if this is indeed a bug), I can provide a demo when I next have free time to write it (i’m usually pretty busy).

What is the expected behavior?

The expected behavior is for different firebaseConnect calls to start watching the provided paths no matter when they are called in the application lifecycle.

Which versions of dependencies, and which browser and OS are affected by this issue? Did this work in previous versions or setups?

Lastest stable version of chrome on ubuntu linux, react-redux-firebase version ^1.5.1, never attempted in previous versions.

Most helpful comment

@andreugrimalt yup, glad to help. Good to know about the typo.

The first one not working kind a totally makes sense since the props it is receiving never change (it won't know to re-render and re-attach listeners).

This should definitely be noted in the example, and maybe we should consider it "dangerous" to use store.getState() in firebaseConnect since this can happen. Dealing with this s actually why at work we usually pass auth as a prop (from an earlier connect) rather than using getStore (which was just added as a convince as per a feature request).

All 12 comments

@scottcrossen Glad to hear you benefit from it! If you are beginning to get familiar with things you may want to try out v2.0.0 which has a more simple API (there is a migration guide) .

Not sure why you wouldn't be seeing the listener change based on the props changing since this code right here makes it so that listeners are reattached on props change (pretty sure we actually depend on this functionality in a few places in our application at work).

Have you tried placing your connect after firebaseConnect? I could be wrong, but placing it before in the HOC stack may be having unintended consequences (docs usually place the connect calls after the firebaseConnect).

Either way, going to try to replicate myself and I'll get back to you. Thanks for reporting.

@prescottprue Wow. I appreciate your prompt response. Unless you're some sort of closet-Asian (a geographical not racist comment), then you were monitoring your GitHub issues at 1-3 a.m. Dedication.

Anyway, seen as nobody else has this issue (I thought that perhaps they might) -- and the fact that I'm a novice with this technology -- I suspect that the most likely cause is me. Though you're welcome to replicate the results, I think the best course of action is for me to make a shareable demo. That way I hopefully can solve the issue myself and your time isn't wasted solving a noob's mistake. I've spent many hours on this problem so far, and I can spare a few more in 1.5 weeks from now to do it (I'm a student and have midterms)

Also, I doubt it's the compose order in the HOC. They execute bottom-to-top. I can still try it though.

Also, I just GitHub-stalked you and found something I want to ask you about. I'll find you on linked-in.

Again, Thanks!

Hi everyone,

I'm having exactly the same issue as you @scottcrossen. The firebaseConnect() only gets called once on the first render and doesn't get called again when the props are updated on the component.

In my case I've found a workaround by reversing the recommended order of composition of firebaseConnect and connect (exactly like you do @scottcrossen) and updating the store. This is the way it works for me:

export default compose(
  connect(state => {
    const uid = state.login.user ? state.login.user.uid : '';
    return {
      entries: state.firebase.data[uid],
      login: state.login,
    };
  }),
  firebaseConnect((props, store) => {
    const state = store.getState();
    const uid = state.login.user ? state.login.user.uid : '';
    return [`${uid}/entries`];
  }),
)(EntryListContainer);

It works because I update the store. If I don't do that, then componentsWillReceiveProps doesn't trigger. I couldn't figure out why yet.

I'm using react-redux-firebase: ^2.0.0-beta.15

Thanks for making this tool btw!

@andreugrimalt I'm glad I'm not the only person with the problem! When you say "update the store" could you be more explicit on that? i.e. how?... or do you have a code snippet you can share?
Unless I'm missing some key redux knowledge, I don't see you doing that in the above example.
I'm really grateful for the assistance.

For sure. An example of how the state in my store looks like:

{
    firebase: // this comes from react-redux-firebase,
    login: [],
}

In my component on componentWillMount I dispatch an action that updates the login field in the store state:

  componentWillMount() {
    this.props.firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.props.dispatch(setUser(user));
      } else {
        // handle user===null here
      }
    });
  }

Then the reducer updates the state (state.login):

    case 'SET_USER':
      return Object.assign({}, state, {
        user: {
          uid: action.uid,
          displayName: action.displayName,
          photoURL: action.photoURL,
        },
      });

When I do that, the firebaseConnect callback triggers and the listener reattaches. What I don't understand is why this doesn't happen when the firebase object gets updated in the store which is what happens when you log in a user for example. I have the feeling that I'm missing something but can't pin it down.

@andreugrimalt Is there a reason that you are doing all of this user management yourself? There is a profile state in the firebaseReducer that is updated onAuthStateChange. We should be able to get this fixed even the way you are doing it, but just wondering.

My Thoughts

The way things currently work is that componentWillReceiveProps runs when firebaseConnect receives props, but listeners will only be detached/reattached if their query config changes (i.e. different path or query params). This makes me think that maybe there is something going on with how we are comparing the query configs. If that is the case, the new query won't be attached, potentially causing the described symptoms.

Other Things To Note

In the case where the store changes, but there are no props that are passed to firebaseConnect changing, the componentWillReceiveProps code will never run because it didn't "receive props".

Unreleated

Did anyone try v2.0.0-beta.17 yet? It probably won't change things for this situation, but it would be great to confirm this is still happening.

@prescottprue thank you so much for your answer really appreciate it.
There's no reason at all to do the user management myself, other than I couldn't make it work.
I'm getting closer to get everything working though. Basically, I'm trying to use what's described in the docs Data that depends on state.
If I do:

export default compose(
  firebaseConnect((props, store) => ([
    `${store.getState().firebase.auth.uid}/entries`
  ])),
  connect(({ firebase: { data, auth } }) => {
    console.log(data, auth);
    return ({
      entries: data[auth.uid] ? data[auth.uid] : [],
    })
  }),
)(EntryListContainer)

data is always undefined.
If I invert the order of firebaseConnect and connect everything works and data gets the correct object:

export default compose(
  connect(({ firebase: { data, auth } }) => {
    console.log(data, auth);
    return ({
      entries: data[auth.uid] ? data[auth.uid] : [],
    })
  }),
  firebaseConnect((props, store) => ([
    `${store.getState().firebase.auth.uid}/entries`
  ])),
)(EntryListContainer)

There's a typo in the example Data that depends on state btw. I can do a PR later.

@andreugrimalt yup, glad to help. Good to know about the typo.

The first one not working kind a totally makes sense since the props it is receiving never change (it won't know to re-render and re-attach listeners).

This should definitely be noted in the example, and maybe we should consider it "dangerous" to use store.getState() in firebaseConnect since this can happen. Dealing with this s actually why at work we usually pass auth as a prop (from an earlier connect) rather than using getStore (which was just added as a convince as per a feature request).

In an attempt to isolate and fix the problem, I upgraded to version ^2.0.0-beta.17 and now the problem doesn't seem to exist. I'm fine marking this issue as resolved unless you think more work is needed.

@scottcrossen Great to know. Going to close, but ready to reopen if someone runs into this with newer versions.

@prescottprue Hi there, first of all great library!
I think i ran in the same problem tho (i am running version 2.0.3)
for some odd reason the 'users' object isn't available in the data object after i run firebase.login()

export default connect(mapStateToProps)(firebaseConnect(['users'])(Main));

but if i reload the site (with the user still logged in) the 'users' object exists

EDIT:
I found a solution for the problem, i have to somewhat 'actively consume' the props.
for clarification i'll post my container definition below

import { connect } from 'react-redux';
import { StoreState } from '../types/index';
import { firebaseConnect } from 'react-redux-firebase';
import { Main, Props } from '../components/Main';

export function mapStateToProps({ firebase }: StoreState) {
    return {
        profile: firebase.profile,
        users: firebase.data.users,
        container: firebase
    };
}
export default connect(mapStateToProps)(
    firebaseConnect((props: Props) => {
        if (props.profile.isLoaded) {
            return ['users'];
        } else {
            return;
        }
    })(Main)
);

@Ammonix I think what you are seeing may be due to the fact that re-renders are not triggered on auth state changing (though that is a current feature request detailed in #367). If you are hoping to have your listener attached after the auth state changes from non existing to existing you will want to wait for auth to load first.

Something like:

const Todos = ({ firebase, todos }) => {
  if (!isLoaded(auth)) {
    return <div>Todos Loading...</div>
  }
  if (isEmpty(auth)) {
    return <div>No Todos Found</div>
  }
  return (
    <div>
      <h1>Todos</h1>
      <ul>
        {
          Object.keys(todos).map((key, id) =>
            <TodoItem key={key} id={id} todo={todos[key]}/>
          )
        }
      </ul>
    </div>
  )
}
export default compose(
  firebaseConnect((props) => [
    { path: 'todos' }
  ]),
  connect((state) => ({ todos: state.firebase.data.todos }))
)(Todos)

Then in your app component wait for auth to load first:

const App = ({ auth }) => {
  if (!isLoaded(auth)) {
    return <div>Loading...</div>
  }
  if (isEmpty(auth)) {
    return <div>Please Login/Signup</div>
  }
  return (
    <div>
      <h1>Todos</h1>
      <Todos />
    </div>
  )
}

export default connect((state) => ({ auth: state.firebase.auth }))(App)

The pattern is outlined in the query docs. It can all can be made much cleaner and more functional using recompose as mentioned further down in the query docs.

Was this page helpful?
0 / 5 - 0 ratings