redux calculated data re-rendering how to fix

53 Views Asked by At

I have a list of cards that take the state tree. I have one selector that gets the list of jobs, then two selectors that use that selection to map and combine an object to pass into the card.

function ProductionJobs(props) {
    const jobData = useSelector(getDataForProductionJobs);
    const dataData = useSelector(getDataForProduction(jobData.map(x=>x.jobsessionkey)));
    const matData =  useSelector(getMatsForProduction(jobData.map(x=>x.jobsessionkey)));
    console.count("renders");
    const combined = jobData.map(x=> {
        const foundData = dataData.find(y=>y.attachedJobKey===x.jobsessionkey);
        const foundMaterial = matData.filter(z=>z.attachedJobkey===x.jobsessionkey);
        const obj = {...x}
        if(foundData) obj.foundData = foundData;
        if(foundMaterial)  obj.material = foundMaterial;      
        return obj;
    });
    const productionCards = combined.map(x=><ProductionJobCard key={x.jobsessionkey} props={x} />)
    return <div className="ProductionJobs">{productionCards}</div>  
}

The problem is - this re-renders unnecessarily. Is there a better way of combining this data on the reducer's side, instead of the component?

1

There are 1 best solutions below

0
On

You can create a container for ProductionJobCard and select combined items in that one using shallowEqual as second argument when filtering matData items.

const {
  Provider,
  useDispatch,
  useSelector,
  shallowEqual,
} = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;

const initialState = {
  productionJobs: [
    { jobSessionKey: 1 },
    { jobSessionKey: 2 },
    { jobSessionKey: 3 },
    { jobSessionKey: 4 },
  ],
  data: [{ id: 1, attachedJobKey: 1 }],
  mat: [
    { id: 1, attachedJobKey: 1 },
    { id: 2, attachedJobKey: 1 },
    { id: 3, attachedJobKey: 2 },
  ],
};
//action types
const TOGGLE_MAT_ITEM = 'TOGGLE_MAT_ITEM';
const TOGGLE_DATA_ITEM = 'TOGGLE_DATA_ITEM';
const TOGGLE_JOB = 'TOGGLE_JOB';
//action creators
const toggleMatItem = () => ({ type: TOGGLE_MAT_ITEM });
const toggleDataItem = () => ({ type: TOGGLE_DATA_ITEM });
const toggleJob = () => ({ type: TOGGLE_JOB });
const reducer = (state, { type }) => {
  if (type === TOGGLE_MAT_ITEM) {
    //toggles matItem with id of 3 between job 1 or 2
    return {
      ...state,
      mat: state.mat.map((matItem) =>
        matItem.id === 3
          ? {
              ...matItem,
              attachedJobKey:
                matItem.attachedJobKey === 2 ? 1 : 2,
            }
          : matItem
      ),
    };
  }
  if (type === TOGGLE_DATA_ITEM) {
    //toggles data between job 1 or 3
    const attachedJobKey =
      state.data[0].attachedJobKey === 1 ? 3 : 1;
    return {
      ...state,
      data: [{ id: 1, attachedJobKey }],
    };
  }
  if (type === TOGGLE_JOB) {
    //adds or removes 4th job
    const productionJobs =
      state.productionJobs.length === 3
        ? state.productionJobs.concat({ jobSessionKey: 4 })
        : state.productionJobs.slice(0, 3);
    return { ...state, productionJobs };
  }
  return state;
};
//selectors
const selectDataForProductionJobs = (state) =>
  state.productionJobs;
const selectData = (state) => state.data;
const selectMat = (state) => state.mat;
const selectDataByAttachedJobKey = (attachedJobKey) =>
  createSelector([selectData], (data) =>
    data.find((d) => d.attachedJobKey === attachedJobKey)
  );
const selectMatByAttachedJobKey = (attachedJobKey) =>
  createSelector([selectMat], (mat) =>
    mat.filter((m) => m.attachedJobKey === attachedJobKey)
  );
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
const ProductionJobCard = (props) => (
  <li><pre>{JSON.stringify(props, undefined, 2)}</pre></li>
);
const ProductionJobCardContainer = React.memo(
  function ProductionJobCardContainer({ jobSessionKey }) {
    //only one item, no need to shallow compare
    const dataItem = useSelector(
      selectDataByAttachedJobKey(jobSessionKey)
    );
    //shallow compare because filter always returns a new array
    //  only re render if items in the array change
    const matItems = useSelector(
      selectMatByAttachedJobKey(jobSessionKey),
      shallowEqual
    );
    console.log('rendering:', jobSessionKey);
    return (
      <ProductionJobCard
        dataItem={dataItem}
        matItems={matItems}
        jobSessionKey={jobSessionKey}
      />
    );
  }
);
const ProductionJobs = () => {
  const jobData = useSelector(selectDataForProductionJobs);
  const dispatch = useDispatch();
  return (
    <div>
      <button onClick={() => dispatch(toggleMatItem())}>
        toggle mat
      </button>
      <button onClick={() => dispatch(toggleDataItem())}>
        toggle data
      </button>
      <button onClick={() => dispatch(toggleJob())}>
        toggle job
      </button>
      <ul>
        {jobData.map(({ jobSessionKey }) => (
          <ProductionJobCardContainer
            key={jobSessionKey}
            jobSessionKey={jobSessionKey}
          />
        ))}
      </ul>
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <ProductionJobs />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

You should not combine the data on the reducer because you will essentially copy the data (combined data is essentially a copy of the data you already have). The combined data is a derived value and such values should not be stored in state but calculated in selectors, re calculate when needed by using memoization (not done here) but if you're interested you can see here how I use reselect for memoizing calculations.

At the moment the filter and find are run on each item but since the outcome is the same the component is not re rendered.