0
votes

SOLVED

@jonahe helped me realise I was storing stale data in my component state resulting in a mismatch between the redux and component store.

@vs1682 pointed out that the store update check is relatively shallow so shouldn't be relied on for deep nested changes.


Original question:

I'm using promise based actions in redux and am running into the following issue: once my state updates (verified per redux logger) the visual component does not reflect this.

Action flow:

  1. Form is submitted (example: the bio property changes)
  2. Smart component triggers a dispatch of an action
  3. Action works as intended and triggers reducer
  4. State updates well (reflected in redux logger in console)
  5. Interface does not reflect the change (but the state certainly does)

As per the above I'm quite confident my actions and reducers are fine. I suspect I'm overlooking something in the redux > visual flow.

The below is isolated code. I took away jsx etc that is not relevant.

Map state to props for the smart component:

export default connect( store => ( { 
    user: store.user ? true : false,
    contacts: store.contacts
} ) )( ContactList )

The smart component

<Person person = { this.state.showingperson } />

The person prop is set through:

showPerson( e ) {
    e.preventDefault( )
    const showingperson = this.props.contacts.object[e.target.id]
    this.setState( { ...this.state, showingperson: showingperson } )
}

The dumb component:

export const Person = ( { person } ) => {
    return <div id="persontable" className="backdrop">
                <div className = 'modal col l6 m12 s12 center'>
                    <p>Bio: { person.bio }.</p>
                </div>
            </div>
}

Edit: Reducer code:

import moment from 'moment'

const contactsReducer = ( state = { array: [], object: {} }, action ) => { 

    switch( action.type ) { 

        case 'UPDATE_FULFILLED':
        case 'GETALL_FULFILLED':
            // action.payload is an array of contacts resulting from a transformed firebase call
            return action.payload ? action.payload : state
        break

        case 'CLEAR_FULFILLED':
            return null
        break

        default:
            return state

     }

 }

 export default contactsReducer
2

2 Answers

2
votes

Connect method is highly optimized. It shallowly compares the combined object returned from mapStateToProps and mapDispatchToProps methods to their previous result. If they are shallowly equal the wrapped component won't update.

To check if this is the case you can add componentWillReceiveProps method to your wrapped component and check whether you are hitting this method.

2
votes

Edit with new info from the comment section below:

Is it the <UserList /> or the <Person /> component that doesn't show the updated data? Because it would make sense if <Person /> didn't update considering it gets the data from the stale data in the internal state:

// this is used to set the internal component state
const showingperson = this.props.contacts.object[e.target.id]
// but after an update of the Redux state, this still points to the 
// old (not updated) object in the internal state
this.setState( { ...this.state, showingperson: showingperson } )

So the the internal state will contain a reference to the old object until the showPerson function is executed again.


Old, wrong answer:

This is commonly due to a mutation of the state in the reducer, causing Redux not to recognize that a change has occurred that should re-render the connected component. (Because of the shallow compare that vs1682 mentioned.)

So in your reducer, make sure that you are not doing anything like this:

case UPDATE: {
  // mutation!
  state.contacts = action.payload;
  return state; 
}

But instead have something like this

case UPDATE: {
  // No mutation. Creating a new object!
  return {...state, contacts: action.payload};
  // or do the equivalent with Object.assign, 
  // Don't forget an empty object as first argument.
}