Search code examples
javascriptreactjscomponentsjsxfiltering

What is the correct way to map and print unique dataset objects in React?


I'm trying to create a filter section for my Side Nav where I can toggle each <li>on or off to show comics of that title only. Currently I am having issues with correctly looping my dataset in order to grab unique name values and printing those as an <li>.

What I've tried so far is mapping the PRODUCTS object, grabbing each name value and storing it in an array, looping through that array length with a seperate index value, and then returning that name value if it meets this condition (arr[i] !== arr[j]).

For example my PRODUCTS dataset might contain 15 different products or objects, 5 named Alita Battle Angel, 5 named Wolverine, and 5 named Spiderman.

expected output: console.log this

(1) Array=[1:'Alita Battle Angel', 2:'Wolverine', 3:'Spiderman'] 

return this

<li> Alita Battle Angel</li>
<li> Wolverine </li>
<li> Spiderman </li>

actual output: console.log this

(15) Array=[ 1:'Alita Battle Angel', 2:'Alita Battle Angel', 3:'Alita Battle Angel', 4:'Alita Battle Angel', 5:'Alita Battle Angel', 6:'Wolverine', 7:'Wolverine', 8:'Wolverine', 9:'Wolverine', 10:'Wolverine', 11:'Spiderman', 12:'Spiderman', 13:'Spiderman', 14:'Spiderman', 15:'Spiderman' ]

return this

<li>Alita Battle Angel</li>

This is my code: (I am also splitting the name value of each so that it returns only the characters before the ',' to make sure that each comic title is entirely unique. Example: "Alita battle Angel, Vol. 1" ---> "Alita Battle Angel")

import React from "react";
import { PRODUCTS } from "../Utilities/PRODUCTS";
import "./SideNav.scss";

const SideNav = (props) => {
    let arr = [];
    return (
        <div className="sideNav-container">
            <div className="sideNav-title"></div>
            <ul>
                {PRODUCTS.map((product) => {
                    // map the PRODUCTS data

                    // loop products data while carrying a singular products 'name'
                    for (let i = 0; Object.keys(PRODUCTS).length > i; i++) {
                        // add that name value to array list
                        arr.push(product.name.split(",", 1));
                        console.log("products length: " + Object.keys(PRODUCTS).length);

                        // if array greater than 1 item
                        if (arr.length > 1) {
                            console.log(arr);
                            // loop the length of that array
                            for (let j = 0; j < arr.length; j++) {
                                // if array[j] not equal to previous array item return name
                                if (arr[i] !== arr[j]) {
                                    let title = `${product.name}`.split(",", 1);
                                    return <li key={[product.id]}>{title}</li>;
                                } else return null;
                            }
                        }
                    }
                })}
            </ul>
        </div>
    );
};

export default SideNav;

The issue is that my code is injecting the entire dataset as a single object. Basically returning my dataset 15 times as a single array. I don't get why it is doing this.

I know using the filter method might be a possible solution and also using the useState hook from react. I've tried both of these but was received some sort of error specific to my file setup.

Any suggestions for solution would be appreciated.


Solution

  • Your code is quite confusing. The reason it's ultimately happening is because you are looping over the PRODUCTS var many times. Once with the top level PRODUCTS.map call, and then again inside the map callback with the for (let i = 0; Object.keys(PRODUCTS).length > i; i++) {.

    You are also maintaining an arr on the outside, which is bad practice since you really want just pure functions. You then loop over this again, so things have got very complicated for no gain.

    Anyway, it's best to take a step back here. map by itself is actually not enough to do the job here. Fundamentally, map will take an array of length n, and apply a transformation to each item, returning a new array that has the same length n.

    What you really want is a reduce, which is capable of looping over each element in the PRODUCTS array and building up a new structure that has a different length.

    You also want to not just get a flat array of name "categories", but also to be able to link those "categories" to the relevant product. So the target structure I've gone for is actually an object where the keys are the categories and the value of those are each product within that category.

    Let's say I have this, using example data:

    [
      { name: "Alita Battle Angel, ep1", id: "1" },
      { name: "Wolverine, ep2", id: "2" },
      { name: "Wolverine, ep4", id: "3" },
      { name: "Spiderman, whatever", id: "4" },
    ].reduce((productsByTitle, product) => {
      const title = product.name.split(",", 1)[0];
      return {
        ...productsByTitle,
        [title]: [...(productsByTitle?.[title] ?? []), product],
      };
    }, {});
    
    

    This will return:

    {
        "Alita Battle Angel": [
            {
                "name": "Alita Battle Angel, ep1",
                "id": "1"
            }
        ],
        "Wolverine": [
            {
                "name": "Wolverine, ep2",
                "id": "2"
            },
            {
                "name": "Wolverine, ep4",
                "id": "3"
            }
        ],
        "Spiderman": [
            {
                "name": "Spiderman, whatever",
                "id": "4"
            }
        ]
    }
    

    From there, it's more straightforward. We can iterate this new map and render what we need.

    I've also gone ahead and also added the nested titles within each category, and made it so you can click on a title to expand it. here it is put together.

    import React, { useState } from "react";
    import { PRODUCTS } from "../Utilities/PRODUCTS";
    import "./SideNav.scss";
    import "./styles.css";
    
    
    const SideNav = (props) => {
      const [openCategory, setOpenCategory] = useState(null);
    
      return (
        <div className="sideNav-container">
          <div className="sideNav-title"></div>
          <ul>
            {Object.entries(
              PRODUCTS.reduce((productsByTitle, product) => {
                const category = product.name.split(",", 1)[0];
                return {
                  ...productsByTitle,
                  [category]: [...(productsByTitle?.[category] ?? []), product]
                };
              }, {})
            ).map(([categoryName, categoryProducts]) => (
              <li key={categoryName}>
                <span
                  style={{ cursor: "pointer" }}
                  onClick={() =>
                    setOpenCategory((prevOpenCategory) =>
                      prevOpenCategory === categoryName ? null : categoryName
                    )
                  }
                >
                  {categoryName}
                </span>
                <ul
                  style={openCategory !== categoryName ? { display: "none" } : {}}
                >
                  {categoryProducts.map(({ id, name }) => (
                    <li key={id}>{name}</li>
                  ))}
                </ul>
              </li>
            ))}
          </ul>
        </div>
      );
    }