1

OK, I am advancing my project from having simple data arrays in the component to an API call for the data, and as things always are in life, the component no longer works. The API is working I can see the data results, but it is just not producing them in the render:

   // function 
   showMainTestimonial(clientId) {
    let results = [];
    let linkButtonNode = null;
    TestimonyApi.getAll()
        .then(function (data) {
            var data = data.data;

            if (data.showMore) {
                linkButtonNode = (
                    <div style={{ margin:15, width:'100%', textAlign:'center' }}>
                        <Link className="button" to={ data.testimonialsLink }>More Testimonials</Link></div>)
            }

            data.testimonials.map(function (testimony, index) {
                let commenter = null;
                let category = null;

                if (testimony.isMain && results.length === 0) {
                    if (data.showCommenterName) {
                        commenter = (
                            <div style={{ textAlign: 'right', fontSize: 16 }}>
                                <i>-- { testimony.commenter }</i>
                            </div>
                        );
                    }
                    if (testimony.category) {
                        category = (
                            <div style={{ textAlign: 'right', fontSize: 16 }}>
                                <i> { testimony.category }</i>
                            </div>
                        );
                    }

                    results.push(
                        <div id="TestimonialArea" key={ index }>
                            <div className="main" id="TestimonialZone">
                                <div id="TestimonialsFeed" className="NavFeed">
                                    <div className="testspcr"></div>
                                    <article className="lists">
                                        <h3>
                                            "{ testimony.title }"
                                        </h3>
                                        <div className="ReviewText">
                                            "{ testimony.comment }"
                                        </div>
                                        { commenter }
                                        { category }
                                    </article>
                                    { linkButtonNode }
                                </div>
                            </div>
                        </div>
                    )
                    console.log('results from function: ' + JSON.stringify(results))
                    return results;
                }
            });
        }.bind(this))
}

// render
render() {
    let clientId = this.props.clientId;
    var results = this.showMainTestimonial(clientId);
    console.log('render results: ' + results);
    return (
        <section>
            { results }
        </section>
    )
}

enter image description here

As you can see the data is there, I am just doing something STUPID.

Any ideas?

Thanks in Advance.

Brigand
  • 84,529
  • 20
  • 165
  • 173
  • 1
    The hint is in the logs. As you can see, the log from your render function is printing first and then the log from inside your promise then handler. You need to rethink the structure of your app. The render function is not the place to do Ajax calls. It should either be in your constructor or event handler, that should set the state of your component, and then render function will render your component based on the state. As a side note, look into flux. That will make it a lot easier to reason about with your app state. – Abhishek Jain Oct 18 '15 at 22:06

3 Answers3

3

You need to take the result of the promise and put it in state, and then have render be based on state.

class Foo extends React.Component {
  constructor(){
    super();
    this.state = {data: null};
  }
  fetchTestimonial(clientId) {
    TestimonyApi.getAll()
      .then((x) => this.setState({data: x}))
  }
  render(){
    if (!this.state.data) return <div>Loading</div>
    return (
      <div>
        {this.state.data.map(f)}
      </div>
    )
  }
}

Note: the arrow function is important, it ensures this is correct in the .then callback. Consider only using arrow functions inside methods because it avoids an entire type of bug.

Brigand
  • 84,529
  • 20
  • 165
  • 173
1

TestimonyApi.getAll / promises are asynchronous. This is a very common problem for people new to JavaScript and has been extensively discussed:

In the context of React, the data should probably be part of the component's state. Instead of returning results (into the void), update the state instead:

this.setState({results});

This will cause the component to rerender. Inside the render method, access this.state.results instead.

You can start the fetching "automatically" by calling the method after the component was mounted. See componentDidMount in Lifecycle Methods.


FYI, the render method should never really fetch data. It is called after the data has changed (either because the component's props or state changed). Maybe reading Thinking in React will help you get a better understanding of this.

Community
  • 1
  • 1
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • At the beginning of the API call if I bind state like this: var data= response.data; this.setState(data); everything is fine but I need to manipulate that data to produce the correct html. After the results push when I try this.setState({results}), I get an error: TypeError: Cannot read property 'setState' of undefined(…) –  Oct 18 '15 at 22:48
  • You have to make sure that `this` refers to the component. And all the stuff you do inside the `.then` callback should probably be inside `render`. – Felix Kling Oct 18 '15 at 22:59
0

I figured out my problem, and refactoring the code makes it clearer to understand.

Basically as FakeRain said, the arrow function was key, although I guess I don't work well with general prose / high altitude theory. No matter, for anyone else struggling.

When you fetch data, and then plan to map it into a specific html 'fragment', split up the fetching and the mapping into 2 different functions, and remember to:

  1. Use the arrow function in the API fetching; (because of 'this')
  2. Call the mapping function in the API function; (because the API call is asynchronous)
  3. Bind the mapping function;
  4. Pass the results of your mapping to state.

So, illustratively speaking:

componentDidMount() {
    this.getAPIData();
}

getAPIData() {
    TestimonyApi.getAll()
        .then((response) => {   <-- don't forget the arrow function
            let data = response.data;
            // call the mapping function inside the API function
            this.showMainTestimonial(data) 
        })
}

showMainTestimonial(data) {
   let results = [];     
   data.map(function(item) {
   // do something with mapped data and push in results
   results.push (

    // customizing the mapping into whatever results you want

    )}.bind(this))  <--- don't forget to bind

  // pass your configured html into state now after mapping
  this.setState({results});
 }