Why are my component's redux-based props not updating?

Viewed 569

in a react-native app that stores data in both a redux store as well as firebase firestore, I have a onSnapshot listener thats listens to a collection. in each document in this collection, there is an object with a couple properties that will change in value over time. ideally, when one of these object property values change, it should be reflected win the redux store, and thus the UI. the problem is, that when one of these values changes, the redux store is not updated.

listener:

export const onAgendaChange = (uid, role, dispatch) => {
    let query;
    if (role == 'A') {
        query = 'a.uid';
    } else {
        query = 'b.uid';
    }
    console.log('(actions/agenda) Firestore Read: getAgenda()');
    return firebase
        .firestore()
        .collection('events')
        .where(query, '==', uid)
        .orderBy('date')
        .onSnapshot((querySnapshot) => {
            const agenda = []
            querySnapshot.docChanges().forEach(change => {
              console.log(`change: ${change.type}`) // does not log anything
            })
            querySnapshot.forEach((doc) => {
                let event = doc.data();
                event.id = doc.id;
                agenda.push(event);
                dispatch({ type: types.LOAD_AGENDA_SUCCESS, payload: agenda });
            });
        });
};

reducer:

const INITIAL_STATE = {
    loading: false,
    events: []
};
export default (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case types.LOAD_AGENDA:
            return {
                ...state,
                loading: true
            };
        case types.LOAD_AGENDA_SUCCESS:
            return {
                loading: false,
                events: action.payload
            };
        default:
            return {
                ...state
            };
    }
};

screen component using this data:

<View style={styles.container}>
  <FlatList
    data={this.props.events}
    extraData={this.props}
    keyExtractor={(item) => {
        return (
            item.toString() +
            new Date().getTime().toString() +
            Math.floor(Math.random() * Math.floor(new Date().getTime())).toString()
        );
    }}
    renderItem={({ item, index }) => {
        const title = this.props.role == 'A' ? item.b.legalName : item.a.legalName;

        const participantCheckInStatus =
            this.props.role == 'A' ? item.checkedIn.b : item.checkedIn.a;

        const currentUserCheckInStatus =
            this.props.role == 'A' ? item.checkedIn.a : item.checkedIn.b;

        console.log("agenda type B: " + item.checkedIn.b + " agenda type A: " + item.checkedIn.a)
        return (
            <Components.AgendaItem
                title={title}
                participant={participantCheckInStatus}
                checkedIn={currentUserCheckInStatus}
                navigation={this.props.navigation}
                evnt={item}
            />
        );
    }}
/>
            </View>

the document's structure in this collection looks something like:

{
  exampleMap: { a: false, b: false },
  string: '',
  string: '',
  map: { ... },
  map: { ... }
}

other notes:

  • whether the document is updated by action or from firebase console, the document is successfully updated in firestore
  • even if the react native application is refreshed, the component using the data in question does not update
  • I have another collection with documents that also have maps. when changing the values in this other collection's map, the component using this data updates properly. they have similar reducers, both reducers are persisted, and the listeners are identical.
  • im also using redux-persist on these relevent reducers ive mentioned, both the working and non-working reducers
  • im using redux-thunk

PECULIAR OBSERVATION:

  • the ONLY time the component receives prop updates properly is if the app install is fresh, meaning the app has been deleted and freshly installed on simulator. any subsequent runs the prop data will not update.

Ive confirmed inside the parent and child components using this redux store, that the document's exampleMap sub-properties value does not change in redux when I change the value in firebase console. so I played around with FlatList's extraData prop, to no avail.

Anyway, any thoughts on how to get these changes to the redux store?

2 Answers

This is the part that is giving you problems:


        .onSnapshot((querySnapshot) => {
            const agenda = []
            querySnapshot.docChanges().forEach(change => {
              console.log(`change: ${change.type}`) // does not log anything
            })
            querySnapshot.forEach((doc) => {
                let event = doc.data();
                event.id = doc.id;
                agenda.push(event);
                dispatch({ type: types.LOAD_AGENDA_SUCCESS, payload: agenda });
            });
        });

The problem is that you are re-using the same agenda array for subsequent dispatches - modifying it in-between.

In your reducer


        case types.LOAD_AGENDA_SUCCESS:
            return {
                loading: false,
                events: action.payload
            }

your are just setting events to this object - which means that when react-redux compares the old events with the new events it's a reference to the same object. RR can't see a difference there.

What you could do to fix this would be to make a copy of that array before saving it to your store:


        case types.LOAD_AGENDA_SUCCESS:
            return {
                loading: false,
                events: [...action.payload]
            }

You need to make the following changes in the reducer:

case types.LOAD_AGENDA_SUCCESS:
  return { ...state, loading: false, events: [...action.payload] };

As your screen component would check the reference of the events props in order to determine whether to re-render or not so you can fix this by changing the reference using the spread operator as shown above.

Also, you should shift the dispatch call to outside the forEach loop so that agenda array is sent to reducer once only with all the items in it instead of being sent again and again for each single item:

  querySnapshot.forEach((doc) => {
    let event = doc.data();
    event.id = doc.id;
    agenda.push(event);
  });
  dispatch({ type: types.LOAD_AGENDA_SUCCESS, payload: agenda });
Related