Alternately Style Items In Deeply Nested Lists With React
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"