Search code examples
javascriptreactjsjsxsemantic-ui-react

Semantic UI + React issue with undefined data


I'm quite new to React. I have JSON file on server, taking data from it via Node.JS. I want to assign the first part of this data to selectedProfile but it says undefined. When I try to assign it says Cannot read property '*AnyProp*' of undefined lower in JSX part of the code even if I try this.state.selectedProfile.general.firstName etc

import React from 'react';
import ReactDOM from 'react-dom';
import { Image, Search, Grid, Menu } from 'semantic-ui-react';

class ContentFeed extends React.Component {


constructor() {
    super();
    this.state = {
        'items': [],
        'prevName': '',
        'i': 0,
        'selectedProfile': []
    }
    this.getItems = this.getItems.bind(this);

}

componentWillMount() {
    this.getItems();
}


getItems() {

    var jsonData = [];

    const xhr = new XMLHttpRequest();

    xhr.onload = () => {
        if (xhr.status === 200) {
            jsonData = JSON.parse(xhr.responseText);
            this.setState({'items': jsonData});
            this.setState({'prevName': jsonData[0].general.firstName + ' ' + jsonData[0].general.lastName});
            document.getElementById(this.state.prevName).style = 'border: 3px solid black';
            this.setState({'selectedProfile': this.state.items[0]});
            console.log(jsonData);
        } else {
            console.log('Error: No 200 OK response');
        }
    }

    xhr.open('get', 'http://localhost:8000/api/item');
    xhr.send();

}

handleItemClick = (e, { name }) => {
    if (name !== this.state.prevName) {
        document.getElementById(name).style = 'border: 3px solid black';
        document.getElementById(this.state.prevName).style = 'border: 1px solid black';
        this.setState({'prevName': name});
        let i = this.state.items.findIndex(element => {
            return (element.general.firstName + ' ' + element.general.lastName) === name;
        });
        this.setState({'i': i});

        this.setState({'selectedProfile': this.state.items[i]});
    }
}

render() {
    return (
        <Grid>
            <Grid.Column width={4} style={{'height': '700px', 'overflowY': 'scroll'}}>
                <Search />              
                    {this.state.items.map( (item, index) => {
                    return (
                        <Menu key={index} fluid vertical tabular>
                            <Menu.Item 
                                key={item.general.firstName + ' ' + item.general.lastName}
                                name={item.general.firstName + ' ' + item.general.lastName} 
                                id={item.general.firstName + ' ' + item.general.lastName} 
                                onClick={this.handleItemClick}
                                style={{border: '1px solid black'}}
                            >
                            <Image src={item.general.avatar} style={{'width': '50px', 'height': '50px'}}/>
                            <p><br />{item.general.firstName + ' ' + item.general.lastName}</p>
                            <p>{item.job.title}</p>
                            </Menu.Item>
                        </Menu>
                    )
                })}
            </Grid.Column>

            <Grid.Column stretched width={12}>
              <div style={{'margin': 'auto'}} id="content">
                    <h2 style={{'marginTop': '10px', 'overflow': 'auto'}}>{this.state.selectedProfile[0]} {this.state.selectedProfile[1]}</h2>
                    <p></p>
                    <Image style={{'margin': '10px', 'float': 'left'}} src={this.state.selectedProfile[2]}/>
                    <p>Company: {this.state.selectedProfile[3]}</p>
                    <p>Title: {this.state.selectedProfile[4]}</p><br />
                    <p>Email: {this.state.selectedProfile[5]}</p>
                    <p>Phone: {this.state.selectedProfile[6]}</p>
                    <p>Address: {this.state.selectedProfile[7]}</p>
                    <p>City: {this.state.selectedProfile[8]}</p>
                    <p>ZIP: {this.state.selectedProfile[9]}</p>
                    <p>Country: {this.state.selectedProfile[10]}</p>
              </div>
            </Grid.Column>
        </Grid>
    );
}
}

Solution

  • Well you seem to have multiple issues here. In your getItems() xhr callback you are doing the following

    jsonData = JSON.parse(xhr.responseText);
    this.setState({'items': jsonData});
    this.setState({'prevName': jsonData[0].general.firstName + ' ' + jsonData[0].general.lastName});
    this.setState({'selectedProfile': this.state.items[0]});
    

    But this.setState is an async function, meaning that upon setting the state, you cannot assume that this.state is already updated. For the sake of argument, if you are setting multiple values in your state, feel free to combine them in one statement, like:

    this.setState({ 
      items: jsonData, 
      selectedProfile: jsonData[0],
      prevName: jsonData[0].general.firstName + ' ' + jsonData[0].general.lastName
    });
    

    However, that is not the only issue here, but rather a matter of data flow, that you seem to have. You should consider that you don't have the data yet upon the first render. So to handle that, you should be checking inside your render function, if there is actually a selectedProfile before trying to render any of its properties. So in that case, you could change render like this:

    render() {
      const { items, selectedProfile } = this.props;
      if (!selectedProfile) {
        // render nothing till the selected profile is there
        return null;
      }
      // the rest of your rendering to return the grid (you can now use items & selectedProfile without the prefix this.state
    }
    

    Which would mean there is no need to set the default state to an empty array (which it isn't anymore, as you are now assigning an object to it, but I guess you missed that when refactoring for previous comments)

    constructor() {
      super();
      this.state = {
        items: [],
        selectedProfile: null
      };
    }
    

    One important note in your code would be DOM manipulation, you shouldn't do DOM manipulation through the traditional way of selecting an item and mutating it, in fact, it is strongly recommended to leave all rendering up to React. There, the classNames library can help you out in conditional className selections.

    Your current implementation of this.handleItemClick will also lose the context to this for the time being, you can choose to bind handleItemClick in your constructor (as you seem to do with getItems, where that one is not necessary), or using the arrow function for the event handler.

    So, if all these points are changed, you kinda come up with a similar code (note that I used to dog api, as it easy to use and has a public api I can write to)

    const Dog = ( { name, content, isSelected, onSelected } ) => {
      return <div onClick={() => onSelected(name)} className={ classNames('breed', {'selected': isSelected}) }>{ name }</div>;
    };
    
    class DogList extends React.Component {
      constructor() {
        super();
        this.state = {
          loading: true,
          selectedBreed: null
        };
      }
      componentWillMount() {
        this.fetchDogs();
      }
      componentDidUmount() {
        this.mounted = false;
      }
      selectBreed( breedName ) {
        this.setState( { selectedBreed: breedName } );
      }
      fetchDogs() {
        fetch('https://dog.ceo/api/breeds/list/all')
          .then( response => response.json() )
          .then( json => {
            this.setState( { breeds: json.message, loading: false } );
          })
          .catch( err => console.error( err ) );
      }
      render() {
        const { loading, breeds, selectedBreed } = this.state;
        if ( loading ) {
          return <div>Loading dogs list</div>;
        }
        return <div className="dogs">{ Object.keys( breeds ).map( key => <Dog key={key} name={key} content={breeds[key]} isSelected={selectedBreed === key} onSelected={(...args) => this.selectBreed(...args)} /> ) }</div>;
      }
    }
    
    const target = document.querySelector('#container');
    ReactDOM.render( <DogList />, target );
    .breed {
      margin: 3px;
      padding: 5px;
    }
    .selected {
      border: solid #a0a0a0 1px;
      background-color: #00ff00;
    }
    <script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
    <script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
    <script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
    <div id="container"></div>