javascriptreactjsreduxredux-toolkitreselect

React using createSelector with dynamic arguments


I created selectors to get a list of users like so:

const selectGroup = (state) => state.group;

const selectGroupUsers = createSelector(
  [selectGroup],
  (group) => group.get('users').toJS()
)

I use it as follows:

const users = useSelector(selectGroupUsers);

These work fine.

Now I am trying to get specific user info using a dynamic path:

const selectUser = createSelector(
  [selectGroup, (_, path)],
  (group, path) => group.getIn(path).toJS()
)

I use this as follows:

const path = ['users', 5, 'data'] // this is a dynamic array value from props

const userData = useSelector(state => selectUser(state, path));

This works, however I get many redux warnings:

Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders.

How do I resolve this?

I tried splitting up the selector as follows but did not work:

const selectGroup = (state) => state.group;

const selectPath = (state, path) => path;

const selectUser = createSelector(
  [selectGroup, selectPath],
  (group, path) => group.getIn(path).toJS()
)

** UPDATE

I rewrote the logic:

const selectUser = (path) => {
  return createSelector(
     [selectGroup, () => path],
     (group, path) => group.getIn(path).toJS()
  )
}

and then:

const userData = useSelector(selectUser(path));

This seems to work! And the errors / warnings went away. I am not sure why. Is this the correct pattern?


Solution

  • The [selectGroup, selectPath] or [selectGroup, (_, path) => path] input selector functions should work, but it depends on the value of path as this typically only works with primitives.

    You appear to be passing an array though, and if you are redeclaring path to be a new ['users', 5, 'data'] array reference each render cycle then this will cause the input selector to change each time and trigger the selectUser selector function to recompute each time.

    If you want to use:

    const selectUser = createSelector(
      [selectGroup, (_, path) => path],
      (group, path) => group.getIn(path).toJS()
    );
    

    Then I'd suggest trying to memoize the path value that is passed to the selector. This might work for you.

    Example:

    const path = React.useMemo(() => {
      return ['users', 5, 'data'];
    }, [/* whatever the appropriate dependencies are */]);
    
    const userData = useSelector(state => selectUser(state, path));
    

    Preferably you can break up the path array values into individual parameters that can be correctly memoized.

    Example:

    const selectUser = createSelector(
      [
        selectGroup,
        (_state, users) => users, // users string
        (_state, _users, value) => value, // value number
        (_state, _users, _value, data) => data, // data string
      ],
      (group, users, value, data) => group.getIn([users, value, data]).toJS()
    );
    
    const userData = useSelector(
      state => selectUser(state, 'users', 5, 'data')
    );
    

    I rewrote the logic:

    const selectUser = (path) => {
      return createSelector(
         [selectGroup, () => path],
         (group, path) => group.getIn(path).toJS()
      )
    }
    

    and then:

    const userData = useSelector(selectUser(path));
    

    This seems to work! And the errors / warnings went away. I am not sure why. Is this the correct pattern?

    You could also use path directly like

    const selectUser = (path) => createSelector(
      [selectGroup],
      (group) => group.getIn(path).toJS()
    );
    

    but the downside to writing your selector like this is that a new memoized selector is internally created each time selectUser(path) is called and internally calls createSelector, so you lose out on the memoization benefits.

    You could memoize the selector function using the useMemo hook.

    Example:

    const memoizedSelectUserByPath = useMemo(() => selectUser(path), [path]);
    
    const userData = useSelector(memoizedSelectUserByPath);