Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing asynchronously acquired data to child props

I am making an app that gets an array of news items from a remote source and displays them on a page.

I have the endpoint, and can make a successful call, proven by console logs, using $.getJSON(). I placed this call into the parent component, because child components will need to use the data.

However, when I pass this data down to a child component, the console error appears:

Uncaught TypeError: Cannot read property 'headline' of undefined

This is because React is trying to render the component even before the data has been passed into it. This makes me think I should first be calling my ajax somewhere other than componentDidMount.

To get around it, I set up a method on the child component that returns the headline if the prop is present:

getHeadline: function () {
    if(this.props.newsItems){
        return this.props.newsItems.headline
    } else {
        return null
    }
},

This feels like a bit of a nasty way around it. Is there a better way, or am I missing something in my code?

var BigStory = React.createClass({

    getHeadline: function () {
        if(this.props.newsItems){
            return this.props.newsItems.headline
        } else {
            return null
        }
    },

    render: function () {
        console.log('props:', this.props);
        console.log('newsItems:', this.props.newsItems);
        return (
            <div className="big-story col-xs-12">
                <div className="col-sm-5">
                    <h1>{this.getHeadline()}</h1>
                    <p>Placeholder text here for now.</p>
                    <p>time | link</p>
                </div>
                <div className="col-sm-7">
                    <img src="http://placehold.it/320x220" alt=""/>
                </div>
            </div>
        );
    }
});

var Main = React.createClass({

    getInitialState: function () {
        return {
            newsItems: []
        }
    },

    componentDidMount: function () {
        this.getNewsItems();
    },

    getNewsItems: function () {
        $.getJSON('http://www.freecodecamp.com/news/hot', (data) => {
            console.log('data sample:', data[0]);
            this.setState({newsItems: data})
        })
    },

    render: function () {
        return (
            <div className="container">
                <div className="main-content col-sm-12">
                    <div className="left-sided-lg-top-otherwise col-lg-8 col-md-12 col-sm-12 col-xs-12">
                        <BigStory newsItems={this.state.newsItems[0]}/>
                    </div>
                </div>
            </div>
        );
    }
});
like image 915
alanbuchanan Avatar asked Mar 14 '23 19:03

alanbuchanan


2 Answers

I would suggest leaving it up to the parent to decide what to do when it is in a "loading" state and leaving BigStory as a "dumb" component that always renders the same assuming it will always receive a valid newsItem.

In this example, I show a <LoadingComponent />, but this could be whatever you need it to be. The concept is that BigStory shouldn't have to worry about edge cases with "receiving invalid data".

var Main = React.createClass({
  // ...
  render() {
    const {newsItems} = this.state;
    // You could do this, pass down `loading` explicitly, or maintain in state
    const loading = newsItems.length === 0;
    return (
      <div className="container">
          <div className="main-content col-sm-12">
              <div className="left-sided-lg-top-otherwise col-lg-8 col-md-12 col-sm-12 col-xs-12">
                  {loading 
                    ? <LoadingComponent />
                    : <BigStory newsItem={newsItems[0]} />  
                  }
              </div>
          </div>
      </div>
    );
  }
});

function BigStory(props) {
  // Render as usual. This will only be used/rendered w/ a valid
  return (
    <div className="big-story col-xs-12">
      <h1>{props.headline}</h1>
      {/* ... */}
    </div>
  )
}

An alternative solution (although I recommend an approach more like above) would be to always make use of the BigStory component in the same way, but provide it a "placeholder story" when there are no stories loaded.

const placeholderNewsItem = {
  headline: 'Loading...',
  /* ... */
};

var Main = React.createClass({
  // ...
  render() {
    const {newsItems} = this.state;
    // Conditionally pass BigStory a "placeholder" news item (i.e. with headline = 'Loading...')
    const newsItem = newsItems.length === 0
      ? placeholderNewsItem
      : newsItems[0];
    return (
      <div className="container">
          <div className="main-content col-sm-12">
              <div className="left-sided-lg-top-otherwise col-lg-8 col-md-12 col-sm-12 col-xs-12">
                  <BigStory newsItem={newsItem} />
              </div>
          </div>
      </div>
    );
  }
});
like image 82
Erik Aybar Avatar answered Mar 16 '23 08:03

Erik Aybar


This is a pretty common scenario. John Carpenter's approach would work. Another approach that I use often would be to create some type of Loading component with a spinner image (or whatever else you might use to signify that data is on its way). Then, while the client waits for the data to arrive, you can render that Loading component. For example:

render: function () {
    if (this.state.newsItems.length === 0) return <Loading />;
    return (
        <div className="container">
            <div className="main-content col-sm-12">
                <div className="left-sided-lg-top-otherwise col-lg-8 col-md-12 col-sm-12 col-xs-12">
                    <BigStory newsItems={this.state.newsItems[0]}/>
                </div>
            </div>
        </div>
    );
}
like image 32
Brandon Avatar answered Mar 16 '23 09:03

Brandon