Search code examples
javascriptreactjsreduxredux-toolkitimmutable.js

React component will not update when using redux and createSelector, not detecting UI changes


I created a selector of users in a group in my immutable redux state:

import { useStore } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit'

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

const memoizedUsers = createSelector(
  [selectGroup],
  (group) => {
    return group.get('users').toJS();
  }
)

const useMemoizedUsers = () => {
  const store = useStore();
  return memoizedUsers(store.getState());
}

and then in my component I am displaying a list of the users with the option to delete a user:

const DeleteButton = ({ rowIndex )} => {
  const dispatch = useDispatch();

  return (
    <Button onClick={() => {
      dispatch({
        type: '@@group/removeUser',
        payload: { rowIndex }
      })
    }}
   >
     Delete
   </Button
  )
}

const columnDefs = [
  {
    headerName: 'Name',
    field: 'name',
  },
  {
    headerName: 'Age',
    field: 'age',
  },
  {
    headerName: '',
    field: 'onDelete',
    cellRenderer: ({ node: { rowIndex } )} => {
      return (
        <DeleteButton rowIndex={rowIndex} />
      )
    }
  }
];

const UserList = () => {
  const users = useMemoizedUsers();

  return {
    <Grid 
      rowData={users}
      columnDefs={columnDefs}
    />
  }
}

When I click delete, I see the redux store being updated ==> from 10 users down to 9 users. However, my UserList component is not rerendering and the UI still displays all 10 Users.

Anyone know how to force the component to update or am I using the createSelector incorrectly?

If I change const users:

const users = useSelector((state) =>
  state.group.get('users')?.toJS()
}

The component rerenders and the user list updates, but I get that redux warning that Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders.

Further, I can rewrite the UserList like this:

const UserList = () => {
  const users = useSelector((state) =>
    state.group.get('users')
  }

  return {
    <Grid 
      rowData={users.toJS()}
      columnDefs={columnDefs}
    />
  }
}

Writing the component this way re-renders the UI and the redux warning goes away, but this defeats the purpose of using the createSelectors.


Solution

  • The useStore hook doesn't actually subscribe to updates from the store. store.getState is basically a one-and-done function call, it gets and returns the current state value from the store, once.

    const useMemoizedUsers = () => {
      const store = useStore();
      return memoizedUsers(store.getState()); // <-- doesn't subscribe to store changes
    }
    

    createSelector actually already creates a memoized selector function, so you just use it directly in a useSelector hook call.

    const selectGroup = (state) => state.group;
    
    const selectGroupUsers = createSelector(
      [selectGroup],
      (group) => group.get('users').toJS()
    )
    
    import { useSelector } from 'react-redux';
    
    ...
    
    const UserList = () => {
      const users = useSelector(selectGroupUsers);
    
      return {
        <Grid 
          rowData={users}
          columnDefs={columnDefs}
        />
      }
    }