Style items in deeply nested lists using React in a different way

Working with react to render nested unordered lists from JSON files has been a challenge. I'm trying to find a way to display them with alternating background colors for each line, making the content easier to read.

1 (white)
2 (gray)
    3 (white)
        4 (gray)
        5 (white)
    6 (gray)
7 (white)

I attempted to achieve this using pure CSS but ran into issues with the nth-of-type() selector only checking the relative index. This resulted in an inconsistent color pattern as shown below:

1 (white)
2 (gray)
    1 (white)
        1 (white)
        2 (gray)
    2 (gray)
3 (white)

Next, I tried incorporating a counter in my recursive rendering function to determine the even and odd lines, but encountered further challenges:

1 (white)
2 (gray)
    4 (gray)
        6 (gray)
        7 (white)
    5 (white)
3 (white)

I understand that flattening the tree structure and utilizing a matching field to establish the even/odd sequence could be a solution, however, it seems inefficient both in terms of time and space.

Answer №1

One way to achieve this task is by manipulating the DOM directly within a useEffect (or useLayoutEffect for synchronous calls) after rendering.

Below is a code snippet that assigns a ref to the list container and utilizes a useEffect with a state as a dependency to iterate over all child li elements, providing cleanup of the manipulations in the return callback.

 const ulContainerRef = React.useRef(null);

  React.useEffect(() => {
    const listItems = ulContainerRef.current.querySelectorAll('li');
    listItems.forEach((item, i) => {
      item.classList.add(i % 2 ? 'even' : 'odd');
    });

    return () => {
      listItems.forEach((item, i) => {
        item.classList.remove('even', 'odd');
      });
    }
  }, [data]);

var initData = [{ id: 1, title: 'Title 1', children: [{ id: 1.1, title: 'Title  1.1' }, { id: 1.2, title: 'Title 1.2' }] }, { id: 2, title: 'Title 2', children: [{ id: 2.1, title: 'Title 2.1' }] }, { id: 3, title: 'Title 3', children: [{ id: 3.1, title: 'Title 3.1' }, { id: 2.2, title: 'Title 3.2' }] }]

const App = () => {
  const [data, setData] = React.useState(initData);
  const ulContainerRef = React.useRef(null);

  React.useEffect(() => {
    const listItems = ulContainerRef.current.querySelectorAll('li');
    listItems.forEach((item, i) => {
      item.setAttribute('data-row', i);
      item.classList.add(i % 2 ? 'odd' : 'even');
    });

    return () => {
      listItems.forEach((item, i) => {
        item.classList.remove('odd', 'even');
      });
    }
  }, [data]);

  const alterData = () => {
    let i = 1;
    setData(prevData => (
      [...prevData.slice(0, i),
      {
        ...prevData[i],
        children: [...prevData[i].children,
        {
          id: +'2.' + (prevData[i].children.length + 1),
          title: 'Title 2.' + (prevData[i].children.length + 1)
        }
        ]
      },
      ...prevData.slice(i + 1)]
    ));
  }

  return (
    <div ref={ulContainerRef}>
      <Ul list={data} />
      <button type='button' onClick={alterData}>Alter Data</button>
    </div>
  )
}

const Ul = ({ list }) => {

  return (
    <ul>
      {list.length > 0 && list.map((item, i) => (
        <li key={item.id}>
          {item.title}
          {(item.hasOwnProperty('children') && item.children.length > 0) &&
            <Ul key={item.id + '_c'} list={item.children} />
          }
        </li>
      )
      )}
    </ul>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById("react")
);
body {
  font-family: monospace;
}

ul {
  list-style-type: none;
  width: 160px;
}

.odd::before {
  content: "(odd: " attr(data-row) ") ";
  background-color: gray;
}

.even::before {
  content: " (even: " attr(data-row) ") ";
  background-color: aquamarine;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Answer №2

This method involves tallying all previous sibling branches (along with their nested children) for each item displayed by the tree. While it allows for a direct mapping of your data structure without flattening it, the performance drawbacks may hinder its practicality in real-world scenarios if the tree is extensive and deeply nested.

The effectiveness of this approach largely hinges on how frequently your `data` undergoes updates. For a static navigation menu where content remains constant post-initial load, encapsulating the code within a memoized component should suffice. However, if the data is subject to frequent modifications, I recommend restructuring the data and caching the computation for more efficiency instead of recalculating during each render cycle as done in this approach.

It's worth noting that this issue isn't related to asynchronous programming. It's not feasible to use `await` mid-way through a `map` call – the data either exists in the mapped array or it doesn't.

const data = [
  {
    name: "Level 1-1"
  },
  {
    name: "Level 1-2",
    children: [
      {
        name: "Level 2-1",
        children: [
          {
            name: "Level 3-1"
          },
          {
            name: "Level 3-2"
          }
        ]
      },
      {
        name: "Level 2-2"
      }
    ]
  },
  {
    name: "Level 1-3"
  }
];

const getSibCount = (itemArr, count = 0) => {
  itemArr.forEach((item) => {
    count += 1;
    item.children && (count += getSibCount(item.children));
  });
  return count;
};

const getClass = (count) => (count % 2 === 1 ? "grey" : "white");

function Tree({ data, count, depth }) {
  return (
    <ul>
      {data.map((item, i, arr) => {
        const newCount = count + getSibCount(arr.slice(0, i));

        return item.children ? (
          <li key={item.name}>
            <p style={{paddingLeft: `${depth*15}px`}} className={getClass(newCount)}>
              {newCount} {item.name}
            </p>
            <Tree data={item.children} count={newCount+1} depth={depth+1} />
          </li>
        ) : (
          <li style={{paddingLeft: `${depth*15}px`}} className={getClass(newCount)} key={item.name}>
            {newCount} {item.name}
          </li>
        );
      })}
    </ul>
  );
}

ReactDOM.render(
  <Tree data={data} count={0} depth={0} />,
  document.getElementById('root')
);
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
  list-style: none;
}

.grey {
  background: grey;
}

.white {
  background: cornsilk;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to incorporate regular React JSX with Material UI, or is it necessary to utilize TypeScript in this scenario?

I'm curious, does Material UI specifically require TypeScript or can we use React JSX code instead? I've been searching for an answer to this question without any luck, so I figured I'd ask here. ...

RxJS emits an array of strings with a one second interval between each emission

Currently, my code is set up to transform an Observable<string[]> into an Observable<string>, emitting the values one second apart from each other. It's like a message ticker on a website. Here's how it works at the moment: const ...

Attribution of image in footer

The image above the footer is not displaying properly. Screenshot: Image appears below the footer https://i.stack.imgur.com/RvSqI.png Source Code .footer-area { background-position: center; position: relative; z-index: 5; } .footer-area::before { ...

Ways to retrieve form data following a post request using JavaScript

Does anyone know how to read form data from testPage.html in testPageResult.html? I have created a html page called testPage.html with a form that has a post action like the following: <h2>HTML Forms</h2> <form action="http://test.com/tes ...

Auto adjusting textarea height as per content length

Is there a way to adjust the height of the text area based on the content entered by the user? I want it to automatically increase as the user types and decrease if they delete content. ...

Nested functions with async-await syntax

I'm attempting to showcase a nested countdown utilizing two nested loops. You can access the complete, functional code on this js.fiddle link. Two key segments are highlighted below: function sleep(ms) { return new Promise(resolve => setTime ...

What methods can be used to prevent the appearance of the ActiveX warning in IE7 and IE8 when dealing with drop

I have a dilemma with my website where I am using JavaScript to modify CSS on hover for specific items. Interestingly, in Firefox everything runs smoothly without any ActiveX message popping up. However, in IE8, I encounter a warning stating "To help prote ...

Exploring Objects using the for-in loop in ReactJS

This code is written in Reactjs. I am attempting to iterate through and print object data, but I keep encountering an error --> TypeError: Cannot read properties of undefined (reading 'state'). Could someone please help me identify what I am d ...

The CSS property :read-only is used on elements that do not have the readonly attribute

Could someone please clarify why the CSS pseudo class :read-only is being added to elements that are not actually readonly? To see an example, check out this link I have tested this in recent versions of Edge, Chrome, and Firefox. All of them apply the i ...

The returned error object from express does not include the error message

I recently posted a question related to an error issue. I am now wondering if I should have edited that question instead. Regarding the code snippet below: app.use((err, req, res, next) => { res.status(err.status || 500); res.json({ message: e ...

Exploring Specific Locations with AmCharts 5 Zoom Feature

Just starting to use AmCharts 5 and I'm struggling to trigger a simple zoom into a location on a map. No matter which tutorial I follow, I can't seem to replicate the results others are getting. If you want to take a look at my Codepen setup to ...

Facing an error when refreshing a page in NextJS while using p5.js

For my website's about page, I'm incorporating the react-p5 library to create a PerlinNoise wave animation. However, I have noticed a strange issue - when I include the p5 component on any page, it redirects me to a 404 error page. But if I navig ...

When utilizing Vue JS, each key value of one computed property can trigger another computed property to run

I have a computed property: getRelatedItem () { return this.allItems.find((item) => { return item.id === this.currentSelectedItemId }) }, Here is an example of the output: relatedItem:Object -KQ1hiTWoqAU77hiKcBZ:true -KQ1tTqLrtUvGnBTsL-M:tr ...

Incorporating ES6 (ECMAScript 2015) modules: bringing in index.js

As I explore the depths of the Internet, I find myself puzzled by the mysterious "index.js" module file. Through the use of babelJS + Node.js or Browserify/Webpack, I am able to effortlessly import an "index.js" module located within a "libs" directory wi ...

Creating a property based on the given values of a different property

Currently, I am grappling with the task of aligning the properties of one interface with the specified values from another interface. For instance interface IHeader: Array<{header: string, label: string}> interface IData: Array<{ /** properties ...

Function in JQuery `each`

I have several images displayed on my ASP.NET page using the following code: for (int i = 0; i < 3; i++) { Image image = new Image(); image.ID = "UpdateStatus"; image.CssClass = "imageCss"; image.ImageUrl = "Bump ...

modify rectangular section in drupal rather than numerical block

Currently, I am attempting to modify blocks within my Drupal 7.x website using the Zen base theme. While I have been successful in adjusting the CSS styles of the blocks based on their numbers in the block.css file, I find this method quite confusing. Is ...

Struggling to get Vuetify tabs to work alongside masonry.js, any solutions available?

As I work on building a photo gallery using vuetify and masonry.js, my goal is to have multiple tabs with images laid out in a masonry style. While using masonry.js for the layout and vuetify for the tabs, I encountered an issue where only the initial tab ...

Explore RxJs DistinctUntilChanged for Deep Object Comparison

I have a scenario where I need to avoid redundant computations if the subscription emits the same object. this.stateObject$ .pipe(distinctUntilChanged((obj1, obj2) => JSON.stringify({ obj: obj1 }) === JSON.stringify({ obj: obj2 }))) .subscribe(obj =& ...

Accessing Elements by Class Name

I've been searching for information on the capabilities of getElementsByClassName for hours. While some sources mention it returns an HTML collection of items, length, and named items, w3schools provides an example where innerHTML is utilized: var x ...