Using reselect with a date field on an object-based reducer

Viewed 456

I have a simple task list app. One of the screens is a "Today & Overdue" list.

The tasks reducer looks like:

{
   "data": {
      123: {
         "id": 123,
         "summary": "blah blah",
         "dueDate": "2020-03-12",
         "completed": true
      },
      456: {
         "id": 456,
         "summary": "some other task",
         "dueDate": "2020-03-12",
         "completed": false
      }
   },
   "byId": [123, 456]
}

My list reducer looks like:

{
   "data": {
      919: {
         "id": 919,
         "name": "Today & Overdue"
      },
      818: {
         "id": 818,
         "summary": "My Cool List"
      }
   },
   "byId": [919, 818]
}

On the "Today & Overdue" list, I need to fetch all tasks where the dueDate is today or older. I tried using reselect to optimize for the performance of the list screen, via:

# Get end of day today
const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());


const getTasksTodayOrOlder = (state) => Object.values(state.tasks.data).filter(task => Date.parse(task.dueDate) <= endOfDay);

But it appears that any time a field in the tasks data changes (i.e. completed or summary), the getTasksTodayOrOlder regenerates the selector.

Is the only way to do this to keep a cache on the tasks reducer; something like byDueDate to keep track of an array of arrays of due dates.


{
   "data": ...,
   "byId": ...,
   "byDueDate": {
       "2020-03-19": [112,123,141, ...],
       "2020-03-20": [922, 939, ...],
   }
}

The date cache seems like a lot of overhead and could get out of sync.

What is the recommended way to handle a reselect that will:

  • Will filter to tasks due today or older
  • Tasks that are not complete
2 Answers

If the output of a selector is a calculated array that uses Object.keys, Object.values or Array.prototype.filter then you can memoize it in the following way:

const { createSelector, defaultMemoize } = Reselect;

const state = [
  { val: 1 },
  { val: 2 },
  { val: 3 },
  { val: 4 },
  { val: 5 },
  { val: 6 },
  { val: 7 },
];
//pass an array to memArray like [a,b], as long as a and b are the same
//  you will get the same array back even if the arrays themselves
//  are not the same like when you use filter, Object.values or Object.keys
const memArray = (() => {
  const mem = defaultMemoize((...args) => args);
  //pass the array that this function receives to a memoized function
  //  as separate arguments so if [a,b] is received it'll call
  //  memoized(a,b)
  return arr => mem(...arr);
})();//this is called an IIFE

const selectHigher = createSelector(
  state => state,
  (_, min) => min,
  (data, min) =>
    memArray(
      Object.values(data).filter(({ val }) => val > min)
    )
);
const one = selectHigher(state, 5);
const twoState = [...state, { val: 0 }];
const two = selectHigher(twoState, 5);
console.log('two is:',two);
console.log('one and two are equal', one === two);
const threeState = [...state, { val: 8 }];
const three = selectHigher(threeState, 5);
console.log('three is:',three);
console.log('three and two are equal', three === two);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>

What does it mean "regenerates the selector"? Where you using this selector? If you are using it in react functional component via hook (useSelector) for example, then:

// selectors

const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());

const getTasks = (state) => state.tasks.data;

const getTasksTodayOrOlder = createSelector(getTasks, tasks => 
  Object.values(tasks)
    .filter(task => Date.parse(task.dueDate) <= endOfDay)
    .map(({ id, dueDate }) => {id, dueDate});

// component

import { shallowEqual, useSelector } from 'react-redux';
.
.
.
const tasksTodayOrOlderList = useSelector(getTasksTodayOrOlder, shallowEqual);

Anytime something in state.tasks.data changes, getTasksTodayOrOlder will be recalculating, but you will not get re render if previous state of tasksTodayOrOlderList is shallow equally to current output of getTasksTodayOrOlder selector (all values inside objects are equal), because we passed second argument shallowEqual to our useSelector function. I used map to remove "tracking" from unnecessary properties from our data object.

And we need to split our selector into two, because we only need to recalculate if our state.tasks.data changes, not when any part of our state changes.

Also, i think your should use endOfDay as arg value to selector, because it's dynamic.

Related