Search code examples
reactjsreduxreact-reduxnormalization

In React and Redux what is the most efficient way to map over an item's Id that is referenced in a normalised state shape?


So I've just started learning redux and in the process read their documentation on 'Normalizing State Shape'.

One of the key takeaway points is:

"Any references to individual items should be done by storing the item's ID."

I've set up my state as they have advised. So in the example below each taskGroup holds a selection of tasks referenced by their ids and each task holds a selection of comments also referenced by their ids.

{
    "taskGroups": {
        "byId": {
            "taskGroup1": {
                "taskGroupId": "taskGroup1",
                "tasks": ["task1", "task2"]
                //etc etc
            },
            "taskGroup2": {
                "taskGroupId": "taskGroup2",
                "tasks": ["task2", "task3"]
                //etc etc
            }
            //etc etc
        },
        "allIds": ["taskGroup1", "taskGroup2", "taskGroup3"]
    },
    "tasks": {
        "byId": {
            "task1": {
                "taskId": "task1",
                "description": "......",
                "comments": ["comment1", "comment2"]
                //etc etc
            },
            "task2": {
                "taskId": "task2",
                "description": "......",
                "comments": ["comment3", "comment4", "comment5"]
                //etc etc
            }
            //etc etc
        },
        "allIds": ["task1", "task2", "task3", "task4"]
    },
    "comments": {
        "byId": {
            "comment1": {
                "id": "comment1",
                "comment": "....."
                //etc etc
            },
            "comment2": {
                "id": "comment2",
                "comment": "....."
                //etc etc
            }
            //etc etc
        },
        "allIds": ["comment1", "comment2", "comment3", "comment4", "comment5"]
    }
}

I understand the theory and see the benefits of having my state structured like this. In practice though, I'm struggling to map over an array of references in an object and a bit lost of where I should be doing it.

What and where is the most efficient way to map over the references to the item's Ids with the actual items?

Should I be doing this on the parent component mapping over all the references before passing them down as props to child components? Or should I pass the references down to child components as props and then map over them there?

Before switching to redux I was using useContext but with a normalised state. I used the following function to filter what tasks were needed for each taskGroup. Thanks to ssube who posted this

export const filterObject = (objToFilter: any, valuesToFind: string) => {
    return Object.keys(objToFilter)
        .filter((key) => valuesToFind.includes(key))
        .reduce((obj, key) => {
            return {
                ...obj,
                [key]: objToFilter[key],
            };
        }, {});
};

Which was then used like so (the same logic was then repeated in my Tasks component to map out comments)

{
    Object.values(taskGroupsById)
        .sort((a, b) => a.sortOrder - b.sortOrder)
        .map((taskGroup) => {
            return (
                <TaskGroup
                    key={taskGroup.taskGroupId}
                    taskGroupTitle={taskGroup.taskGroupTitle}
                    tasks={filterObject(
                        tasksById,
                        taskGroupsById[`${taskGroup.taskGroupId}`].tasks
                    )}
                    handleDrawer={handleDrawer}
                    findTaskStatus={findTaskStatus}
                    findAssignedToTask={findAssignedToTask}
                />
            );
        });
}

This works ok but I'm not sure if its counter intuitive, as it's beeing calculated in multiple instances of the TaskGroup component instead of just once.

Is there a better method to achieve this? I've tried to create a selector in my slices to recreate this but can't seem to work out how to map over multiple references in an array (as opposed to just one reference as a string).

Any help would be much appreciated even if it is just a nudge in the right direction. Thanks!


Solution

  • Should I be doing this on the parent component mapping over all the references before passing them down as props to child components? Or should I pass the references down to child components as props and then map over them there?

    I would recommend the second approach. This can potentially minimize re-renders and it fits with Redux best practice Connect More Components to Read Data from the Store. As explained in the docs:

    Prefer having more UI components subscribed to the Redux store and reading data at a more granular level. This typically leads to better UI performance, as fewer components will need to render when a given piece of state changes.

    For example, rather than just connecting a <UserList> component and reading the entire array of users, have <UserList> retrieve a list of all user IDs, render list items as <UserListItem userId={userId}>, and have <UserListItem> be connected and extract its own user entry from the store.

    This applies for both the React-Redux connect() API and the useSelector() hook.


    You have a taskGroup object which looks like this:

    {
      taskGroupId: "taskGroup1",
      tasks: ["task1", "task2"]
    ...
    

    So it is trivial to pass down the array of task ids to your <TaskGroup> component:

    <TaskGroup
      key={taskGroup.taskGroupId}
      taskGroupTitle={taskGroup.taskGroupTitle}
      taskIds={taskGroup.tasks}
    ...
    

    Now there are two ways that your TaskGroup can handle looking up the task objects.

    If you don't need any sorting, then your TaskGroup doesn't need to select the task objects. It can simply iterate through the ids and defer the selecting to a SingleTask component, like in the <UserListItem userId={userId}> example from the docs.

    In your TaskGroup component, you will have something like:

    <ul>
      {taskIds.map(taskId => (
         <SingleTask key={taskId} taskId={taskId} />
      ))}
    </ul>
    

    Then each SingleTask is responsible for accessing its own data. You can define a selector function, though this one is also simple enough to write inline.

    const selectTaskById = (state, taskId) => state.tasks.byId[taskId];
    
    const SingleTask = ({ taskId }) => {
    
       const task = useSelector(state => state.tasks.byId[taskId]);
       const { description, comments } = task;
    
       return (
         <li>{description}</li>
       );
    }
    

    If you need to apply sorting or filtering which depends on the properties of the individual task then you will need to select all of the task objects in the TaskGroup. Create a selector that takes your array of ids and returns an array of objects.

    Your existing filterObject function is not particularly efficient because your state is already so well structured. You do not need to use any Array.includes() statements. You can simply look up the objects by their keys.

    const selectTasksByIds = (state, taskIds) => {
      const tasksById = state.tasks.byId;
      return taskIds.map(id => tasksById[id]);
    }
    

    In your TaskGroup component, you will have:

    const tasks = useSelector(state => selectTasksByIds(state, taskIds));