Reputation: 129
Here is a sample function called index()
that I use to return an index of headers from a Markdown file. As you can see, with only 3 levels of headers, its quickly getting out of hand.
I tried a variety recursion ideas, but ultimately failed at all attempts to compile in create-react-app
.
Could anyone offer any guidance as to how to clean this up and maybe even allow for an infinite/greater levels of nesting?
An array is stored in this.state.index
which resembles the array below.
[
{
label: 'Header title',
children: [
{
label: 'Sub-header title'
children: [...]
}
]
}
]
Then that data is used in the function below to generate a ul
index.
index() {
if ( this.state.index === undefined || !this.state.index.length )
return;
return (
<ul className="docs--index">
{
this.state.index.map((elm,i=0) => {
let key = i++;
let anchor = '#'+elm.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{elm.label}</AnchorScroll></label>
<ul>
{
elm.children.map((child,k=0) => {
let key = i+'-'+k++;
let anchor = '#'+child.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{child.label}</AnchorScroll></label>
<ul>
{
child.children.map((grandchild,j=0) => {
let key = i+'-'+k+'-'+j++;
let anchor = '#'+grandchild.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{grandchild.label}</AnchorScroll></label>
</li>
);
})
}
</ul>
</li>
);
})
}
</ul>
</li>
);
})
}
</ul>
);
}
Like I said, this is a mess! I'm new to React and coding in general so sorry if this is a silly question.
Upvotes: 2
Views: 4349
Reputation: 31024
Seems like you want a recursive call of a component.
For example, component Item
will render it self based on a condition (existence of the children
array).
Running example:
const data = [
{
label: 'Header title',
children: [
{
label: 'Sub-header title',
children: [
{ label: '3rd level #1' },
{
label: '3rd level #2',
children: [
{ label: 'Level 4' }
]
}
]
}
]
}
]
class Item extends React.Component {
render() {
const { label, children } = this.props;
return (
<div>
<div>{label}</div>
<div style={{margin: '5px 25px'}}>
{children && children.map((item, index) => <Item key={index} {...item} />)}
</div>
</div>
)
}
}
const App = () => (
<div>
{data.map((item, index) => <Item key={index} {...item} />)}
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
Just for completeness, i added another example but this time we handle the isOpen
property outside the Item
's local component state, in a higher level.
This can be easily moved to a redux
reducer or any other state management library, or just let a higher level component like the App
in this case manage the changes.
So, to handle changes of a recursive component you would probably write a recursive handler:
const data = [
{
label: 'Header title',
id: 1,
children: [
{
label: 'Sub-header title',
id: 2,
children: [
{ label: '3rd level #1', id: 3 },
{
label: '3rd level #2',
id: 4,
children: [
{ label: 'Level 4', id: 5 }
]
}
]
},
]
},
{
label: 'Header #2',
id: 6,
children: [
{ label: '2nd level #1', id: 7, },
{
label: '2nd level #2',
id: 8,
children: [
{ label: 'Level 3', id: 9 }
]
}
]
}
]
class Item extends React.Component {
toggleOpen = e => {
e.preventDefault();
e.stopPropagation();
const {onClick, id} = this.props;
onClick(id);
};
render() {
const { label, children, isOpen, onClick } = this.props;
return (
<div className="item">
<div
className={`${children && "clickable"}`}
onClick={children && this.toggleOpen}
>
<div
className={`
title-icon
${isOpen && " open"}
${children && "has-children"}
`}
/>
<div className="title">{label}</div>
</div>
<div className="children">
{children &&
isOpen &&
children.map((item, index) => <Item key={index} {...item} onClick={onClick} />)}
</div>
</div>
);
}
}
class App extends React.Component {
state = {
items: data
};
toggleItem = (items, id) => {
const nextState = items.map(item => {
if (item.id !== id) {
if (item.children) {
return {
...item,
children: this.toggleItem(item.children, id)
};
}
return item;
}
return {
...item,
isOpen: !item.isOpen
};
});
return nextState;
};
onItemClick = id => {
this.setState(prev => {
const nextState = this.toggleItem(prev.items, id);
return {
items: nextState
};
});
};
render() {
const { items } = this.state;
return (
<div>
{items.map((item, index) => (
<Item key={index} {...item} onClick={this.onItemClick} />
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.item {
padding: 0 5px;
}
.title-icon {
display: inline-block;
margin: 0 10px;
}
.title-icon::before {
margin: 12px 0;
content: "\2219";
}
.title-icon.has-children::before {
content: "\25B6";
}
.title-icon.open::before {
content: "\25E2";
}
.title-icon:not(.has-children)::before {
content: "\2219";
}
.title {
display: inline-block;
margin: 5px 0;
}
.clickable {
cursor: pointer;
user-select: none;
}
.open {
color: green;
}
.children {
margin: 0 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Upvotes: 6
Reputation: 16441
I don't have good test data, but try this out:
index() {
if ( this.state.index === undefined || !this.state.index.length ) {
return null;
}
const sections = this.state.index.map(this.handleMap);
return (
<ul>
{sections}
</ul>
)
}
handleMap(elm, i) {
const anchor = '#'+elm.label.split(' ').join('_') + '--' + i;
return (
<li key={i}>
<label><AnchorScroll href={anchor}>{elm.label}</AnchorScroll></label>
<ul>
{
elm.children.map(this.handleMapChildren)
}
</ul>
</li>
);
}
handleMapChildren(child, i) {
let anchor = '#'+child.label.split(' ').join('_') + '--' + i;
return (
<li key={i}>
<label><AnchorScroll href={anchor}>{child.label}</AnchorScroll></label>
<ul>
{
child.children.map(this.handleMapChildren)
}
</ul>
</li>
);
}
Upvotes: 0