Ad

Successful Dispatch Does Not Cause Re-render Even Though State Has Been Changed

- 1 answer

Despite a successful update of my state (checked through console.log and redux devtools) I cannot get my views to re-render

You can view the code at https://github.com/attendanceproject/djattendance/tree/attendance-redux/ap/static/react-gulp/app

Most of the code is in the scripts folder, the most important parts pertaining to my problem are below. I'm trying to re-render every time the previous or next button is pressed in the WeekBar component so that the date in there updates accordingly.

container code

class Attendance extends Component {
  render() {
    const { dispatch, trainee, date, events, rolls, slips, selectedEvents } = this.props
    console.log('this.props', this.props)
    return (
      <div>
        <div>
        <Trainee trainee={trainee} />
        <WeekBar  
          date={date} 
          onPrevClick={date => dispatch(prevWeek(date))}
          onNextClick={date => dispatch(nextWeek(date))}/>
        </div>
        <hr />
      </div>
    )
  }
}

Attendance.propTypes = {
  trainee: PropTypes.shape({
    name: PropTypes.string,
    id: PropTypes.number,
    active: PropTypes.bool
  }).isRequired,
  date: PropTypes.object.isRequired,
  events: PropTypes.array.isRequired,
  rolls: PropTypes.array.isRequired,
  slips: PropTypes.array.isRequired,
  selectedEvents: PropTypes.array
}

function select(state) {
  return {
    trainee: state.trainee,
    date: state.date,
    events: state.events,
    rolls: state.rolls,
    slips: state.slips,
    selectedEvents: state.selectedEvents,
  }
}

export default connect(select)(Attendance)

component code

export default class WeekBar extends Component {
  render() {
    console.log("render props", this.props)
    // var startdate = this.props.date.weekday(0).format('M/D/YY');
    // var enddate = this.props.date.weekday(6).format('M/D/YY');
    return (
      <div className="btn-toolbar" role="toolbar">
        <div className="controls btn-group">
          <button className="btn btn-info"><span className="glyphicon glyphicon-calendar"></span></button>
        </div>
        <div className="controls btn-group">
          <button className="btn btn-default clndr-previous-button" onClick={(e) => this.handlePrev(e)}>Prev</button>
          <div className="daterange btn btn-default disabled">
            {this.props.date.weekday(0).format('M/D/YY')} to {this.props.date.weekday(6).format('M/D/YY')}
          </div>
          <button className="btn btn-default clndr-next-button" onClick={(e) => this.handleNext(e)}>Next</button>
        </div>
      </div>
    );
  }

  handlePrev(e) {
    console.log("hello!", e)
    this.props.onPrevClick(this.props.date)
  }

  handleNext(e) {
    this.props.onNextClick(this.props.date)
  }
}

WeekBar.propTypes = {
  onPrevClick: PropTypes.func.isRequired,
  onNextClick: PropTypes.func.isRequired,
  date: PropTypes.object.isRequired,
}

reducer code

var date = moment()
function handleWeek(state = date, action) {
  switch (action.type) {
    case PREV_WEEK:
      console.log('PREV_WEEK')
      return Object.assign({}, state, {
        date: action.date.add(-7, 'd')
      })
    case NEXT_WEEK:
      return Object.assign({}, state, {
        date: action.date.add(7, 'd')
      })
    default:
      return state
  }
}

export default handleWeek
Ad

Answer

I haven’t taken a close look but you seem to be using Moment.js dates as part of your model. Specifically, onNextClick dispatches an action: dispatch(nextWeek(date)).

The action creator just passes the Moment.js date along:

export function nextWeek(date) {
    return {type: NEXT_WEEK, date}
}

Finally, the reducer mutates the date object by calling add:

return Object.assign({}, state, {
  date: action.date.add(7, 'd') // wrong! it's mutating action.date
})

From Moment.js add documentation:

Mutates the original moment by adding time.

However we emphasize in Redux documentation that reducers must be pure, and that the state must never be mutated, or React Redux won’t see the changes. This is what enables Redux to be efficient because it only re-renders what it knows to have changed.

The solution I suggest is to stop using Moment.js as a part of your state. Use regular JavaScript Date objects, make sure to never mutate them, and only use Moment.js inside your components’ render methods.

Finally, passing data in action derived from the current state is an anti-pattern. Your action currently looks like this:

{type: NEXT_WEEK, date}

But this is too much information! The reducer already knows the current date from the state so there is no need to pass it.

Instead, you can fire an action without the date:

{type: NEXT_WEEK}

and teach your reducer to use the current date when calculating a new one.

Assuming you changed your code to keep a Date object in the state, you can use vanilla JS Date API (it’s not very nice though because Dates are also mutable):

// create a copy of the date object
let newDate = new Date(state.date.getTime());

// mutating here is fine: we mutate a new object
newDate.setDate(newDate.getDate() + 7);

return Object.assign({}, state, {
  date: newDate
})

Alternatively you can use a wonderful new library called date-fns which embraces immutability:

import addDays from 'date-fns/add_days';

// ...

return Object.assign({}, state, {
  date: addDays(state.date, 7) // non mutating! :D
})

If you take care to never mutate the state or the action and always create new objects when data changes, React Redux will correctly update the React component in response to those changes.

Ad
source: stackoverflow.com
Ad