0

I'm stuck with this and I cannot figure out what I'm doing wrong. I'm trying to do a Wikipedia Viewer (as explained here on Freecodecamp.com), basically a simple search of Wikipedia articles.

I've already done it in HTML/CSS/JS(jQuery) and now since I'm learning React I'm trying to recreate it in React.

I've split up my components, I'm using fetch to get data using Wikipedia API and everything is rendering, except the search results. I just cannot display the end result, which is the point of the whole "app".

Using React Dev Tools I already established that data is being fetched and props are being passed to child component and I've tested to see if a dummy child component would render and it did, so rendering all components isn't a problem, I'm just missing some step somewhere to get it to render that component with all the necessary data.

All code is in this code sandbox. I've set it up the way I have all set up locally so that you can clearly see how I'm doing things.

Any help is appreciated!

Sorry if the question is a little long, with all the text and code. I've also included all the code here split by files if that's more convenient for some people.

App.jsx

import React from 'react';
import WikiHeader from './Header';
import WikiSearch from './Search';
import OutputList from './OutputList';
import 'purecss';
import 'purecss/build/grids-responsive-min.css'
import './App.css';

var resultsArray = [];
const wikiAPI = "https://en.wikipedia.org/w/api.php?&";
var wikiHeaders = {
  action: "query",
  format: "json",
  origin: "*",
  prop: "info|extracts",
  exsentences: 2,
  exlimit: 5,
  generator: "search",
  inprop: "url",
  gsrsearch: "",
  gsrnamespace: 0,
  gsrlimit: 5,
  exintro: 1
};

// Format headers to be used for a query string
function toQueryString(obj) {
  var parts = [];
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i]));
    }
  }
  return parts.join("&");
}

class App extends React.Component {

  constructor() {
    super();

    this.state = {
      results: []
    }

    this.ajaxCall = this.ajaxCall.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  // AJAX using fetch
  ajaxCall() {
    resultsArray = []; // clear array every time ajax is called so that it doesn't just add new items to previous items
    toQueryString(wikiHeaders);

    fetch(wikiAPI + toQueryString(wikiHeaders)).then(function(response) {
      return response.json();
    }).then(function(json) {
      for (var prop in json.query.pages) {

        // push response items to array so that state isn't directly changed (not changing state recommended by Facebook)
        resultsArray.push({
          title: json.query.pages[prop].title,
          url: json.query.pages[prop].fullurl,
          extract: json.query.pages[prop].extract
        });
      }
    }).catch(function(ex) {
      console.log('Json parsing failed', ex); // show error if JSON parsing fails
    });

    this.setState({results: resultsArray}); // set state.results to resultsArray
  }

  handleChange(e) {
    wikiHeaders.gsrsearch = e.target.value; // get input value and store it
  }

  handleSubmit(e) {
    e.preventDefault();
    console.log("Hey, this is a submit event!"); // show if function is being called

    // Call fetch
    this.ajaxCall();

    // Clear the search box after submit
    document.getElementById("search-input").value = "";
  }


  render() {
    return (
      <div className="full-wrapper pure-g">
        <div className="pure-u-1">
          <WikiHeader />
          <WikiSearch onChange={this.handleChange} onSubmit={this.handleSubmit} />
          <OutputList results={this.state.results} />
        </div>
      </div>
    );
  }
}

export default App;

Header.jsx

import React from 'react';

class WikiHeader extends React.Component {
  render() {
    return (
      <h1 className="site-title">Wikipedia Viewer</h1>
    );
  }
};

export default WikiHeader;

Search.jsx

import React from 'react';

class WikiSearch extends React.Component {

  render() {
    return (
      <div className="search-input">
        <form className="pure-form" onSubmit={this.props.onSubmit}>
          <div className="pure-u-1 pure-u-md-3-4">
            <input type="text" id="search-input" onChange={this.props.onChange} className="pure-input pure-u-1" placeholder="Input your search term here..."></input>
          </div>
          <div className="pure-u-1 pure-u-md-1-4 button-wrapper">
            <button type="submit" id="search-button" className="pure-button pure-button-primary pure-u-1 pure-u-md-23-24">Search</button>
          </div>
        </form>
        <a href="https://en.wikipedia.org/wiki/Special:Random" alt="Open a Random Wikipedia article" target="_blank" rel="noopener noreferrer">or see a random Wikipedia article</a>
      </div>
    );
  }

};

export default WikiSearch;

OutputList.jsx

import React from 'react';
import OutputItem from './OutputItem';

class OutputList extends React.Component {
  render() {

    var finalResult = this.props.results.map((result, index) => {
      return (
        <OutputItem
          key={index}
          title={result.title}
          url={result.url}
          extract={result.extract}
        />
      );
    });

    return (
      <div id="search-output" className="pure-g">
        <ul className="article-list pure-u-1">
          {finalResult}
        </ul>
      </div>
    );
  }
}

export default OutputList;

OutputItem.jsx

import React from 'react';

class OutputItem extends React.Component {
  render() {
    return (
      <div>
        <li className="pure-u-1">
          <a href={this.props.url} target="_blank" rel="noopener noreferrer">
            <h2>{this.props.title}</h2>
            {this.props.extract}
          </a>
        </li>
      </div>
    );
  }
}

export default OutputItem;
Tim Hutchison
  • 3,483
  • 9
  • 40
  • 76
Marko Hologram
  • 195
  • 2
  • 9
  • In App.jsx, the ajax call and the setState methods are both asynchronous. While the ajax fetch is occurring, the setState method is called, passing in the _original_ empty resultsArray because the ajax fetch hasn't completed. Move this.setState line into the ajax fetch's first 'then', right after the for loop. 'this' will no longer be defined so just change the function in then to an arrow function. Not posting as an answer because of a possible duplicate: https://stackoverflow.com/questions/36648245/react-setstate-not-updating-state-after-ajax-request – thenormalsquid May 30 '17 at 17:46
  • Here's the edited code with the moved setState function and the promise's `then`s converted to arrow functions: https://codesandbox.io/s/g5Q6MZlRG – thenormalsquid May 30 '17 at 17:54
  • @thenormalsquid ahhh that was it. I know ajax and setState are both async and I knew I had to setState after ajax, placed it in the first "then", but I forgot the arrow function. I guess I have to brush up my knowledge about "this" keyword and it's context. I came across that answer you posted, but I didn't really work it out from there. Thank you so much! – Marko Hologram May 30 '17 at 18:10
  • No problem! A pretty good resource on understanding scoping: https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/ And this is a pretty good post about arrow functions: http://2ality.com/2012/04/arrow-functions.html – thenormalsquid May 30 '17 at 18:19
  • 1
    Possible duplicate of [React setState() not updating state after $.ajax() request](https://stackoverflow.com/questions/36648245/react-setstate-not-updating-state-after-ajax-request) – Marko Hologram May 30 '17 at 18:28

2 Answers2

0

I think I managed to make it work on your sandbox.

this.setState({results: resultsArray}); is done in the wrong time. It has to wait for the AJAX call to be complete. I placed it right after your for (var prop in json.query.pages) { loop, thus within the ajax resolution callback.

A resulting problem is that this no longer refers to the Component Instance, so this.setState becomes undefined. To avoid that problem I turned ).then(function(json) { into an arrow function, so that it does not override this.

Here you can see my updated sandbox.

Freeman Lambda
  • 3,567
  • 1
  • 25
  • 33
  • You and @thenormalsquid answered at the same time, both very helpful. I don't know if I should mark this as the answer because of a possible duplicate as commented by thenormalsquid on my question ? If I mark it as the answer it might be easier for someone to see the solution in the future. – Marko Hologram May 30 '17 at 18:17
  • A bit unlikely that someone will come to your question in the future, because it is a very specific setup, with a very generic solution. You might as well close this question IMO, and it wouldnt hurt future users. Glad we were able to help you anyways. – Freeman Lambda May 30 '17 at 18:22
0

If I add some logging to your current app and run it, I can observe that your finalResults variable is empty, even though the query works. This is because the resultsArray variable inside the fetch is not the same as the one outside due to Javascript contexts.

You can first hoist the old context in var that = this; and then use it exactly when you need to do another setState.

https://codesandbox.io/s/KZvXP7kKn

Alex Palcuie
  • 4,434
  • 3
  • 22
  • 27
  • I would like to point out that `var that = this` is now an obsolete technique, since the coming of ES6 arrow functions. Otherwise it is a good workaround in the absence of ES6 features (not a latest version browser). – Freeman Lambda May 30 '17 at 17:52