skip to Main Content

I was wondering which solution is more efficient. I have an array of objects, let’s say, cars. Each of these cars have a status: NEW, PURCHASED and DAMAGED. I need to display 3 tabs: tab with new cars, with purchased cars and with damaged cars. Unfortunately the backend doesn’t give already filtered response, I have to filter it manually on the frontend. So my initial solution was something like that:


const newCars = useMemo(() => {
    const arr = cars
        .filter((car) => car.status === 'new')
        .map((it) => ({
            id: it.id,       
        }));
    return arr;
}, [cars]);


then I just use newCars array, and map it to display some JSX content:

newCars.map((item, index) => (
    <CarItem
    key={item.id}                                           
    />

Filtering the cars array is done 3 times. I thought – would Map be more efficient? Let’s say something like this:

const carsMap = useMemo(() => {
    const result = new Map<string, []>();

    const statuses = ["new", "purchased", "damaged"];

    for (let i = 0; i < statuses.length; i++) {
        const arr = cars
            .filter((car) => car.status === statuses[i])
            .map((it) => ({
                id: it.id,
                //...mapping props here
            }));

        result.set(statuses[i], arr);
    }

    return result;
}, [cars]);

and then I access for example new cars by using:


carsMap.get("new").map((item, index) => (
    <CarItem
    key={index}/>
   ))

Which solution is better (in terms of clean code and performance)?

Both solutions returns the same output, but I dont know which is better.

3

Answers


  1. The Map approach!

    You only perform the filtering and mapping of the original cars array once. After that, you have direct access to pre-filtered arrays for each status without needing to re-filter the main array multiple times. This can be more efficient compared to filtering the cars array separately for each tab.

    Login or Signup to reply.
  2. If you care about performance I would suggest using Array#reduce().

    Currently, even in your second approach with a Map you are still iterating 3 times, because for every status you are running .filter() on the original array, so essentially you are iterating the same amount of times in your both solutions.

    Using .reduce() will iterate only one time. Here’s how you can do it:

    const carsMap = useMemo(() => {
        return cars.reduce((res, curr) => {
          if (res.has(curr.status)) {              // if the status already exists in the result map...
            const statuses = res.get(curr.status); // grab the reference to the array and...
            statuses.push(curr);                   // push the new car.
          } else {
            res.set(curr.status, [curr]);          // else, set a new status array
          }
          
          return res;
        }, new Map<string, []>());
    }, [cars]);
    

    and then you can access it like this (same as your second approach):

    carsMap.get("new").map((item, index) => (
        <CarItem
        key={index}/>
       ))
    
    Login or Signup to reply.
  3. Here is an example of converting a flat list of items into a grouped-by-status object. I used Lodash’s groupBy function to simplify the code below.

    Pay attention to each effect and memo hook. It monitors the changes to the data as it it converted from a list, to an object, and finally an array of <Tab> components.

    This object can then be converted to a list of tab objects identified by a name. Each tab can then hold a child that represents a car list component.

    The entire flow can be found below:

    const { useCallback, useEffect, useMemo, useState } = React;
    const { groupBy } = _;
    
    const fetchCars = () => Promise.resolve([
      { "id":  1, "make": "Dodge",       "model": "Viper",         "year": 1991, "status": "purchased" },
      { "id":  2, "make": "Chevrolet",   "model": "Corvette",      "year": 1953, "status": "purchased" },
      { "id":  3, "make": "Lamborghini", "model": "Countach",      "year": 1974, "status": "purchased" },
      { "id":  4, "make": "Porsche",     "model": "911",           "year": 1964, "status": "purchased" },
      { "id":  5, "make": "Ferrari",     "model": "Daytona",       "year": 1968, "status": "new"       },
      { "id":  6, "make": "Nissan",      "model": "Z-Car",         "year": 1969, "status": "new"       },
      { "id":  7, "make": "Toyota",      "model": "Supra Mark IV", "year": 1994, "status": "new"       },
      { "id":  8, "make": "Honda",       "model": "NSX",           "year": 1990, "status": "damaged"   },
      { "id":  9, "make": "Mazda",       "model": "MX-5 Miata",    "year": 1989, "status": "damaged"   },
      { "id": 10, "make": "MG",          "model": "MGB",           "year": 1962, "status": "damaged"   },
    ]);
    
    const tabOrder = ["new", "purchased", "damaged"];
    
    const Tab = ({ active, name, onClick }) => {
      return (
        <div className="Tab" data-active={active} data-name={name} onClick={onClick}>
          {name}
        </div>
      );
    };
    
    const TabBar = ({ activeName, onClick, tabData }) => {
      return (
        <div className="TabBar">
          {tabData.map(({ name, data }) => (
            <Tab key={name} active={name === activeName} name={name} onClick={onClick} />
          ))}
        </div>
      );
    };
    
    const TabView = ({ activeTab, children, onClick, tabData }) => {
      const activeName = activeTab || tabData[0].name;
      const activeIndex = tabData.findIndex(t => t.name === activeName);
      return (
        <div className="TabView">
          <TabBar activeName={activeName} onClick={onClick} tabData={tabData} />
          <div className="TabContent">
            {children[activeIndex]}
          </div>
        </div>
      );
    };
    
    const CarInfo = ({ id, make, model, year }) => {
      return (
        <div className="CarInfo" data-id={id}>
          {year} {make} {model}
        </div>
      );
    };
    
    const CarList = ({ cars }) => {
      return (
        <div className="CarList">
          <ul>
            {cars.map(({ id, make, model, year }) => (
              <li key={id}>
                <CarInfo id={id} make={make} model={model} year={year} />
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    const App = ({ title }) => {
      const [carsByStatusMap, setCarsByStatusMap] = useState(null);
      const [activeTab, setActiveTab] = useState(null);
      
      useEffect(() => {
        fetchCars().then(cars => setCarsByStatusMap(groupBy(cars, 'status')))
      }, []);
      
      const tabClick = useCallback((e) => {
        setActiveTab(e.target.dataset.name);
      }, []);
      
      const tabData = useMemo(() => tabOrder.map(key => ({
        name: key,
        data: carsByStatusMap ? (carsByStatusMap[key] || []) : []
      })), [carsByStatusMap]);
      
      const tabs = useMemo(() => tabData.map(({ name, data }) => (
        <CarList key={name} cars={data} />
      )), [tabData]);
        
      return (
        <div>
          <TabView activeTab={activeTab} onClick={tabClick} tabData={tabData}>
            {tabs}
          </TabView>
        </div>
      );
    };
    
    ReactDOM
      .createRoot(document.getElementById("root"))
      .render(<App />);
    *, *::before, *::after { box-sizing: border-box; }
    
    * { color: #FFF; }
    
    html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; }
    
    #root {
      display: flex;
      align-items: center;
      justify-content: center;
      background: #222;
    }
    
    .TabView {
      display: flex;
      flex-direction: column;
      border: thin solid #333;
      width: 80vw;
      height: 80vh;
      background: #444;
    }
    
    .TabBar {
      display: flex;
      background: #111;
    }
    
    .Tab {
      display: flex;
      padding: 0.5rem;
      background: #222;
      text-transform: capitalize;
    }
    
    .Tab[data-active="true"] {
      background: #444;
    }
    
    .Tab:hover {
      cursor: pointer;
      background: #555;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

    Note: Since the Babel version here is older, the following:

    data: carsByStatusMap ? (carsByStatusMap[key] || []) : []
    

    May be simplified to:

    data: carsByStatusMap?.[key] ?? []
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search