1
votes

The scenario I'm facing is I have an array of jobs stored as Redux state. I have a container which accesses the data with Redux connect and uses parameter from this.props.params to then find the appropriate Job to then hand down to it's children as a prop.

Within the Child Component the Job triggers an action which updates the job and then is merged and updates the jobs in the store. This seems to be working fine but when the container re-renders the child does not. I can see that is has something to do with the fact that I don't store job as props or state.

My question is what is the best (Redux)way to handle this?

So if I change JobShow to include jobs={jobs} it re-renders as jobs has updated. This seems to indicate that state is not mutating, I also can confirm that the container itself re-renders and even re-runs renderView() but doesn't re-render JobShow.

Container:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import JobShow from './job_show';


class JobContainer extends Component {
  constructor(props){
    super(props);
  }

  renderView(job, customer){
    let viewParam = this.props.params.view;
    switch(viewParam){
      case "edit": return <JobEdit customer={customer} job={job}/>
      case "notes": return <JobShow customer={customer} activeTab={"notes"} job={job}/>
      case "quote": return <JobShow customer={customer} activeTab={"quote"} job={job}/>
      case "invoice": return <JobShow customer={customer} activeTab={"invoice"} job={job}/>
      default: return <JobShow customer={customer} activeTab={"notes"} job={job}/>
    }
  }
  render() {
    let { jobs, customers } = this.props;

    if (jobs.length < 1 || customers.length < 1){
      return <div>Loading...</div>;
    }
    let job = jobs.find(jobbie => jobbie.displayId == this.props.params.jobId);
    let customer = customers.find(customer => customer._id.$oid == job.customers[0]);

    return (
      <div className="rhs">
        {this.renderView(job, customer)}
      </div>
    );
  }
}
JobContainer.contextTypes = {
  router: React.PropTypes.object.isRequired
};
function mapStateToProps(state) {
  return  { 
    jobs: state.jobs.all,
    customers: state.customers.all
  };
}

export default connect(mapStateToProps, actions )(JobContainer);

Reducer Snippet:

import update from 'react-addons-update';
import _ from 'lodash';

export default function(state= INITIAL_STATE, action) {
  switch (action.type) {
    case FETCH_ALL_JOBS:     
      return { ...state, all: action.payload, fetched: true };

    case UPDATE_JOB_STATUS:

      let newStatusdata = action.payload;

      let jobToUpdate = state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId);
      jobToUpdate.status = newStatusdata.newStatus;
      jobToUpdate.modifiedAt = newStatusdata.timeModified;

      let updatedJobIndex = _.indexOf(state.all, state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId));
      let updatedState = update(state.all, {$merge: {[updatedJobIndex]: jobToUpdate}});

      return { ...state, all: updatedState}
2
If nothing else, it looks like you're mutating your state with the lines jobToUpdate.status = ..... Accidental mutation is almost always the reason for components not re-rendering. See redux.js.org/docs/FAQ.html#react-not-rerendering. Also, what do the jobs and your initial state look like?markerikson
So if I change JobShow to include jobs={jobs} it re-renders as jobs has updated but handing props the child does't need seems to be the wrong way. This seems to indicate that state is not mutating, I also can confirm that the container itself re-renders and even re-runs the renderView function but just doesn't re-render JobShow.Dom Hede
Seeing more code might help, but your jobToUpdate lines in the reducer snippet are directly mutating that job object, rather than making a copy, only modifying the copy, and returning that copy. So yes, if you're trying to pass that job object itself to a child component as a prop, the reference itself hasn't changed and the child component will not update.markerikson
I thought I had the mutating state issue solved with $merge from react-addons-update, also jobs is intialized as an empty array [].Dom Hede
Big thanks @markerikson. You were absolutely correct!Dom Hede

2 Answers

1
votes

Looks like you figured it out yourself, but you may find a slight refactoring improves the clarity of what you're doing. Here are some examples of how you might change the structure of your reducers using the object assign operation:

case ActionTypes.EDIT_TODO:
  return Object.assign({}, 
    state, 
    { text: action.text }
  )

From: https://github.com/mjw56/redux/blob/a8daa288d2beeefefcb88c577a7c0a86b0eb8340/examples/todomvc/reducers/todos.js

There's more in the official redux docs as well (redux.js.org/docs/recipes/UsingObjectSpreadOperator.html)

You may also find writing tests and using a package like deepFreeze useful, as it will tell throw an error if you accidentally mutate state without realizing.

1
votes

The problem as pointed out in comments by @markerikson was that I was mutating state. Using the $set function from react-addons-update I successfully updated the content.

The new reducer snippet looks like this:

case UPDATE_JOB_STATUS:

  let newStatusdata = action.payload;

  let jobToUpdate = state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId);
  let updatedJob = update(jobToUpdate, {status:{$set: newStatusdata.newStatus}})
  updatedJob = update(updatedJob, {modifiedAt:{$set: newStatusdata.timeModified}})

  let updatedJobIndex = _.indexOf(state.all, state.all.find(jobbie => jobbie.displayId == newStatusdata.displayId));
  let updatedState = update(state.all, {$merge: {[updatedJobIndex]: updatedJob}});

  return { ...state, all: updatedState}