In this post I want to share with all of you something I consider very important and that I’ve noticed in lots of code repositories regarding the use of state selectors and the useSelector hook. I assume you already have some knowledge or experience working with state management libraries specifically Redux, hope to make it very clear and without further ado, let’s begin.

Imagine you have a state like this:

state: {
  tasks: {
    list: [],
    sortBy: '',
    filters: [],
    apiFilters: '',
    ...
  },
  taskDetails: {
    selectedTask: {},
    dirtyTask: {},
    ...
  }
}

And then in your components you do something like this to get your state and use it:

import { shallowEqual, useSelector } from 'react-redux';

...
export const MyComponent = () => {
// Selectors
const { list } = useSelector((state) => state?.tasks, shallowEqual);

// some code ...

// return render list ...
}

So… What’s wrong with this?

I’ve seen this in lots of places, and the issue is that it causes multiple unnecessary re-renders, why? The shallow equal compares every field inside the selected object (tasks in this example) to identify if it has changed, if it considers it did then it will re-render the component that has the selector.

This means if you only need the list from the tasks state and from any other place the sortBy or the apiFilters change, then any component accessing the list with the code above will be re-rendered unnecessarily.

How do we avoid this?

With this approach:

// NOTE: most likely we won't need shallowEqual for the list array
const list = useSelector((state) => state?.tasks?.list);

This way we access only what we need (the list itself) and the reference or shallow comparison is done properly avoiding unnecessary re-renders.

Oh! What if I need multiple fields from the state?

Then:

const { list, sortBy } = useSelector(({ tasks = {} } = {}) => ({ 
    list: tasks.list, sortBy: tasks.sortBy
  }), shallowEqual);

Note that if you want to avoid shallowEqual for some of the selected objects you could just separate the calls.

const list = useSelector((state) => state?.tasks?.list);
const list = useSelector(({ tasks = {} } = {}) => ({
    apiFilters: tasks.apiFilters,
    sortBy: tasks.sortBy
  }),
  shallowEqual
);

But then I suggest going further to avoid so much code (which could also be duplicated) for getting some state objects, and if you extract each selector logic as custom hooks you end up with a lot of specific state selectors, so how do we come up with a general generic and reusable solution?

import get from 'lodash/get';

export const getSelectField = (stateName) => (
  fieldName = '',
  defaultValue,
) => ({ [stateName]: state }) =>
  Array.isArray(fieldName)
    ? fieldName.map((field, i) => get(
        state,
        field,
        Array.isArray(defaultValue) ? defaultValue?.[i] : defaultValue
      )
    )
    : get(state, fieldName, defaultValue);

This is a function that returns another function (the selector itself), hence we call it getSelectField.

It is a totally generic and reusable general solution for every component and side effect (if using redux sagas) in any app, and to make it simpler you can define the selector in every slice and export it from there to avoid having to do that also multiple times.

As a side note, if you don’t know what I mean by “slice”, you might not be aware of Redux Toolkit which is an amazing library that will allow your team to work with Redux in a simpler way with easy setup and scalability.

So in your reducer’s slice:

 export const selectTasks = getSelectField('tasks');

Here you just go ahead and create a generic and reusable selector hook to use in components:

export const useSelectTasks = (
  fieldName,
  defaultValue,
  equalityFn = shallowEqual
  ) => useSelector(
    selectTasks(fieldName, defaultValue),
    equalityFn ? equalityFn : undefined
  );

Note that for the sake of demonstrating we set shallowEqual to be used by default as the equalityFn, you can avoid that if you consider it not necessary for your selector.

Now in your component you just do:

const list = useSelectTasks('list');

Or for selecting multiple state objects at once:

// "..." here is meant to be any other state object that you want
const [list, sortBy, ...] = useSelectTasks(['list', 'sortBy', ...]);

What about selecting nested objects?

The good thing about this is that since we are using get from lodash you can actually access elements at any level in the state by passing parent.child.anotherChild as fieldName.

So if we had a state such as:

state: {
  taskDetails: {
    selectedTask: {
      metadata: {
        parentTaskGroup: 'some-parent-id',
        subtasksCount: 3
      }
    },
    dirtyTask: {},
    ...
  }
}

Assuming we had a useSelectTasksDetails selector hook, we could select the parentTaskGroup by doing:

const parentTaskGroup = useSelectTasksDetails(
  'selectedTask.metadata.parentTaskGroup'
);

Or for selecting multiple state objects at once.

// "..." here is meant to be any other state object that you want
const [parentTaskGroup, subtasksCount, ...] = useSelectTasksDetails([
  'selectedTask.metadata.parentTaskGroup',
  'selectedTask.metadata.subtasksCount',
  ...
]);

IMPORTANT: For this to work as expected, ensure you always select ONLY what you need. Don’t select state objects that contain fields that you won’t use, if you do so then everytime any of those get updated it will trigger a rerender even though you’re not actually using that field/object.

I hope it was clear enough, and you see the benefits of staying DRY.

Final Code

/**
 * Why use this? It will prevent unnecessary rerenders hence improving performance, it also reduces the amount of code needed to select data from the store
 * hence making it simpler and keeping your codebase smaller and easier to maintain.
 *
 * IMPORTANT: For this to work as expected, ensure you always select ONLY what you need. Don't select state objects that contain fields that you won't use, if you do so
 * then everytime any of those get updated it will trigger a rerender even though you're not actually using that field.
 *
 * @description Generic Redux State Selector Helper.
 * ### Example
 * ```javascript
 * // reducer.js
 * import { getSelectField } from 'somePlace';
 * import { useSelector, shallowEqual } from 'react-redux';
 *
 * const name = 'someState';
 * selectSomeState = getSelectField(name);
 * // useSomeState.js hook. NOTE: You can create a hook for each main state
 * export const useSelectSomeState = (fieldName, defaultValue, equalityFn = shallowEqual) => useSelector(selectSomeState(fieldName, defaultValue), equalityFn);
 *
 * // someComponent.js
 * import { useSelectSomeState } from 'slices/someSlice.js'
 *
 * export const SomeComponent = () => {
 *   const someField = useSelectSomeState('someField');
 *   ...
 * }
 * export const SomeOtherComponent = () => {
 *   const someSubField = useSelectSomeState('someField.someSubField');
 *   ...
 * }
 * export const AnotherComponent = () => {
 *   const [firstField, secondField, thirdField] = useSelectSomeState(['firstField', 'secondField', 'thirdField']);
 *   ...
 * }
 *
 * ```
 * @param {string} stateName the redux store's state name of the state you want to access.
 * @returns {Function} A *selectField* function that returns a selector function to be used inside a *useSelector* hook or a *select* side effect
 */
export const getSelectField = (stateName) => (
  fieldName = '',
  defaultValue,
) => ({ [stateName]: state }) =>
  Array.isArray(fieldName)
    ? fieldName.map((field, i) => get(
        state,
        field,
        Array.isArray(defaultValue) ? defaultValue?.[i] : defaultValue
      )
    )
    : get(state, fieldName, defaultValue);