Skip to content Skip to sidebar Skip to footer

Alternately Style Items In Deeply Nested Lists With React

So I'm using react to render NESTED unordered lists from json files. I wish to display them with alternating background color each line for readability. So something like this: 1 (

Solution 1:

It seems most straightforward to do this with direct DOM manipulation in a useEffect (or useLayoutEffect if you need the synchronous call) after render.

Here is a snippet which applies a ref to the list container and implements a useEffect with a state dependency that queries and iterates over all children li elements with cleanup of the manipulations passed 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' }] }]

constApp = () => {
  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]);

  constalterData = () => {
    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 (
    <divref={ulContainerRef}><Ullist={data} /><buttontype='button'onClick={alterData}>Alter Data</button></div>
  )
}

constUl = ({ list }) => {

  return (
    <ul>
      {list.length > 0 && list.map((item, i) => (
        <likey={item.id}>
          {item.title}
          {(item.hasOwnProperty('children') && item.children.length > 0) &&
            <Ulkey={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;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script><divid="react"></div>

Solution 2:

This approach works by counting all previous sibling branches (and their nested children) for each item rendered by the tree. While it will allow you to directly map your data structure inline (no flattening), the performance cost of doing this if the tree is sufficiently large and deep could make it impractical in a real life use case.

I think it really depends how often your data is going to be refreshed. If this is just for a navigation menu where the content does not change after the initial load, then wrapping the below in a memoised component would work fine. If you're expecting the data to change frequently, however, then I really would suggest flattening the data structure and memoising the calculation instead which can be written more efficiently, rather than trying to calculate it on the fly with each render as this approach does.

A side note: this problem really has nothing to do with asynchronous programming. It is not possible to await in the middle of 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"
  }
];

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

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

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

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

ReactDOM.render(
  <Treedata={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;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script><divid="root"></div>

Post a Comment for "Alternately Style Items In Deeply Nested Lists With React"