Lux
Lux

Reputation: 129

How to handle recursive array mapping with React components?

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

Answers (2)

Sagiv b.g
Sagiv b.g

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

Chase DeAnda
Chase DeAnda

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

Related Questions