4

Sorry if this is a dumb/obvious question, I'm new to Javascript/React. I'm trying to create a menu filled with JSON objects that I get from an API call. Since the JSON data is a bunch of nested objects (I'm talking hundreds of objects), I figured out how to recursively call the objects and make them show up in an unordered list. I can't seem to figure out how to add an onClick function that, when clicked, will show the next level down in the nested objects.

I will provide some screenshots to give you an idea of what I have and what I'm trying to achieve. The recursion part is tripping me up on how I can get this working. Does anyone have ideas?

class Menu extends React.Component {
  state = {
    categories: []
  };

  makeMenuLayer = layer => {
    const layerKeys = Object.entries(layer).map(([key, value]) => (
      <ul>
        {key}
        {this.makeMenuLayer(value)}
      </ul>
    ));
    return <div>{layerKeys}</div>;
  };

  componentDidMount() {
    axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
      this.setState({ categories: response.data });
    });
  }

  render() {
    const { categories } = this.state;
    return <div>{this.makeMenuLayer(categories)}</div>;
  }
}

this is what currently shows up with this code. I'm trying to make it so that only the first layer of objects show up, and then when each element is clicked on, the next level gets displayed. For example if I click on Apparel, It displays Accessory, Clothing, Eyeglasses, etc. and if I click on Accessory, it will display Necktie and Umbrella.

current state

here's a screenshot of the data I'm working with displayed in the console enter image description here

2 Answers 2

1

Probably not the most efficient, but I just thought of using state to hide/unhide the nested objects.

Try this

import React from 'react'

class Menu extends React.Component {
  state = {
    categories: [],
    objectKeys: null,
    tempKeys: []
  }

  makeMenuLayer = layer => {
    const { objectKeys } = this.state
    const layerKeys = Object.entries(layer).map(([key, value]) => (
      <ul key={key}>
        <div onClick={() => this.handleShowMore(key)}>{key}</div>
        {objectKeys[key] && this.makeMenuLayer(value)}
      </ul>
    ))
    return <div>{layerKeys}</div>
  }

  handleShowMore = key => {
    this.setState(prevState => ({
      objectKeys: {
        ...prevState.objectKeys,
        [key]: !this.state.objectKeys[key]
      }
    }))
  }

  initializeTempKeys = layer => {
    Object.entries(layer).map(([key, value]) => {
        const newTempKeys = this.state.tempKeys
        newTempKeys.push(key)
        this.setState({ tempKeys: newTempKeys })
        this.initializeTempKeys(value)
      }
    )
  }

  initializeObjectKeys = () => {
    const { tempKeys } = this.state
    let tempObject = {}

    tempKeys.forEach(tempKey => {
      tempObject[tempKey] = true
    })

    this.setState({ objectKeys: tempObject })
  }

  async componentDidMount () {
    const res = await fetch('https://www.ifixit.com/api/2.0/categories')
    const categories = await res.json()
    this.initializeTempKeys(categories)
    this.initializeObjectKeys()
    this.setState({ categories })
  }

  render () {
    const { categories } = this.state
    return <div>{this.makeMenuLayer(categories)}</div>
  }
}

export default Menu

Sign up to request clarification or add additional context in comments.

2 Comments

Hey Ryan thanks again for your help. I was wondering if you would be able to help me with this. I want to add arrows next to each element that has another layer below it (essentially, if this element has children, display an arrow to indicate so). Do you know would I go about doing that here?
You can add a checker for value. If it's not an empty array, return some icon. Something like {value.length > 0 && <div>arrow</div>}
1

There are a number of ways this can be done, however the general idea is to track some extra state to determine which layer in your menu hierarchy is currently visible, and which is not.

A quick and reasonably simple way to do this would be to add an extra map called hidden to your components state, which would track the visibility of menu layers in your menu tree.

Something like this might work for you:

class Menu extends React.Component {
    state = {
    categories: [],
    /* Add extra state to track which layers are visible or not */
    hidden: {}
    };

    /* Define a local helper function that toggles the layer by key */
    toggleMenuLayer = (key) => {

        /* A state change is required to cause a re-render of the menu */
        this.setState(state => {

            const { hidden } = state
            const isHidden = hidden[ key ];

            /* Add or update hidden state for key in hidden map */
            return { hidden : { ...hidden, key : !isHidden }}
        });
    }

    /* Define a local helper function that determines if a layer is visible by its key */
    isLayerToggled = (key) => {

        /* If "not" hidden then layer is visible, or "toggled" */
        return !this.state.hidden[ key ]
    }

    makeMenuLayer = (parentKey, layer) => {
        const layerKeys = Object.entries(layer).map(([key, value]) => {

            /* Compose a unique key for this item based on it's position in the hierarchy */
            const itemKey = `${parentKey}.${key}`;

            return (
                <ul>
                { /* Add a toggle button for this layer */ }
                <button onClick={() => this.toggleMenuLayer(itemKey)}>{key}</button>

                { /* If layer is toggled then display it's contents */ }
                { this.isLayerToggled(itemKey) && this.makeMenuLayer(value) }
                </ul>
            )
        });
        return <div>{layerKeys}</div>;
    };

    componentDidMount() {
        axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
            this.setState({ categories: response.data });
        });
    }

    render() {
        const { categories } = this.state;
        return <div>{this.makeMenuLayer(categories)}</div>;
    }
}

Hope the comments in the code help!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.