javascriptreactjsreduxreact-reduxreselect

Creating a createSelector in Redux that does not rerender unrelated components


I have a simple document maker that allows users the create multiple fields of two types (input field and label field). They can also rearrange the field via drag and drop. My data structure is based on the Redux guide on data normalization.

LIST field is input type (e.g. children id 21 and 22) - onChange will dispatch action and allows the input text to modified without re-rendering other fields.

With LABEL field we allow users to select from a dropdown list of only 3 labels (Danger, Slippery, Flammable). Selecting one with remove it from the dropdown list to prevent duplicate label. For example, if there are "Danger" and "Slippery" in the field, the dropdown field will only show one option "Flammable". To achieve this, I use createSelector to get all children and filter them base on their parent (fieldId). This gives me an array of existing labels ["Danger","Slippery"] in that fieldId. This array will then be used to filter from a fixed dropdown array of three options with useEffect.

Now whenever I update the input text, it will also re-render the LABEL field (based on the Chrome React profiler).

It does not affect performance but I feel like I am missing something with my createSelector.

Example:

export const documentSlice = createSlice({
  name: "document",
  initialState: {
    fields: {
      1: { id: 1, children: [11, 12] },
      2: { id: 2, children: [21, 22] },
    },
    children: {
      11: { id: 11, type: "LABEL", text: "Danger", fieldId: 1 },
      12: { id: 11, type: "LABEL", text: "Slippery", fieldId: 1 },
      21: { id: 21, type: "LIST", text: "", fieldId: 2 },
      22: { id: 22, type: "LIST", text: "", fieldId: 2 },
    },
    fieldOrder:[1,2]
  },
});

createSelector

export const selectAllChildren = (state) => state.document.children;
export const selectFieldId = (state, fieldId) => fieldId;

export const getChildrenByFieldId = createSelector(
  [selectAllChildren, selectFieldId],
  (children, fieldId) => {
    const filterObject = (obj, filter, filterValue) =>
      Object.keys(obj).reduce(
        (acc, val) =>
          obj[val][filter] !== filterValue ? acc : [...acc, obj[val].text],
        []
      );
    const existingChildren = filterObject(children, "fieldId", filterId);
    return existingChildren;
  }
);

Solution

  • After more reading up, this is what finally works for me. Hopefully someone will find it useful.

    1. Given the normalized data, the object reduce function can be simplified.
    slice.js 
    
    export const getChildrenByFieldId = createSelector(
      [selectAllChildren, selectFieldId],
      (children, fieldId) => {
    
        // get the children from fields entry => array [11,12]
        const childrenIds = state.document.fields[fieldId].children
    
        let existingChildrenText = [];
    
        // loop through the created array [11,12] 
        childrenIds.forEach((childId) => {
          // pull data from the individual children entry
          const childText = children[childId].text;
          existingChildrenText.push(childText);
        });
        return existingChildrenText;
    
      }
    );
    
    1. To prevent re-render, we can use shallowEqual comparison on the output array to compare only the values of the array.
    //app.js 
    
    import { shallowEqual, useSelector } from "react-redux";
    
    const childrenTextData = useSelector((state) => {
      return getChildrenByFieldId(state, blockId);
    }, shallowEqual);