Help with simple react app (random quote machine)

Hi,
I’m currently going through the Front End Development Library certificate, and I’m building the Random Quote Machine.
I know React is overkill for this, but I’ve decided to go with it anyway to test my knowledge so far, before starting to study Redux.
Anyway, I build the App locally using create-react-app (I don’t really like Codepen), and I got all the basic functionality working and passing the tests.
I’m having an issue though (more than one actually, but let’s try to solve this one): as generating a random index for fetching the quotes (I have them in an array in a separate file) can sometimes result in quotes repetition, I’ve decided to generate the array order of shuffled indexes which are then used to fetch the quotes.
The array is generated in the componentDidMount method (line 22), and from there passed into the main component QuoteBox state (line 34).
In the render() method (line 45) I have a variable quote which should be assigned the current quote;
however, when I try to access the quotesArr like so:

let quote = '"' + quotesArr[this.state.order[this.state.quoteCount]].quote + '"';

the app breaks and I have the following error:

quotesArr[this.state.order[this.state.quoteCount]] is undefined.

For debugging purposes, I’m visualizing the index I’m using to access the quotesArr in the p element in line 58, and it shows correctly.

I suspect the problem is in the syntax but I don’t understand what is going on. Any ideas? Here’s the code:

import React from 'react';
//import { react } from '@babel/types';
import ReactFCCtest from 'react-fcctest';
import './styles/App.css';
import { quotesArr } from './quotes.js'; /*quotesArr = [ { quote: '...', author: '...', from: '...' }, {...}, {...} ];
                                           not every quote obj has a 'from' key */


class QuoteBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      order: [],
      quoteCount: 0
    }
    this.getNewQuote = this.getNewQuote.bind(this);
  }


  //on load, generate a new array of shuffled indexes to fetch quotes from quotesArr:
  componentDidMount() {
    let shuffleIdxs = () => {
      let order = [];
      for (let i = 0; i < quotesArr.length; i++) { order.push(i) };
      for (let i = order.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        [order[i], order[j]] = [order[j], order[i]];
      }
      return order;
    }
    //fill order array with shuffle indexes:
    this.setState({
      order: shuffleIdxs(),
      quoteCount: 0
    });
  }

  getNewQuote() {
    this.setState(state => ({
      quoteCount: state.quoteCount + 1
    }))
  }

  render() {
    //this works; shows the quotes in the original order, as quoteCount increases:
    //let quote = '"' + quotesArr[this.state.quoteCount].quote + '"';
    //this doesn't work and throws error:
    let quote = '"' + quotesArr[this.state.order[this.state.quoteCount]].quote + '"';
    let author = quotesArr[this.state.quoteCount].author;
    let from = quotesArr[this.state.quoteCount].from ? ', ' + quotesArr[this.state.quoteCount].from : ''; //display quotesArr's 'from' key value if present

    return (
      <div id="quote-box">
        <p id="text">{quote}</p>
        <p id="author">{author}<em>{from}</em></p>
        <p>quote count: {this.state.quoteCount};</p>
        <p>order: {this.state.order[this.state.quoteCount]}</p>
        <div className="buttons-div">
          <button>
            {/* href="twitter.com/intent/tweet" passes the test but doesn't work. The following works: https://twitter.com/share?ref_src=twsrc%5Etfw */}
            <a id="tweet-quote" href="twitter.com/intent/tweet" target="_blank" data-show-count="false">Tweet</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
          </button>
          <button id="new-quote" onClick={this.getNewQuote}>New Quote</button>
        </div>
      </div>
    )
  }
}


function App() {
  return (
    <div className="App">
      <ReactFCCtest />
      <header className="App-header">
        <h1>Stellar quotes machine</h1>
      </header>
      <QuoteBox />
    </div>
  );
}

export default App;

A simpler way to fetch a different number from the one you currently show would be make a while loop, which generates a random number until its different from the one you currently render. That is how i solved this same issue on my Quote Machine app. I used jQuery tho. You can put this random quite generator in a function for convenience.
Would look something like this:

let quotes=10 //your quotes array length
function newRandom(curNum){
  let newNum=Math.floor(Math.random()*quotes)
  while (curNum===newNum){
    newNum=Math.floor(Math.random()*quotes)
  }
  return newNum
}

console.log(newRandom(5)) //will return anything from 0-9 but 5

Im not sure if that will assist with the problem you are handling.

1 Like

I just wanted to point out that the array is zero-indexed, so maybe

1 Like

Initial state has:

order: [],
quoteCount: 0

and then when you try to do this:

quotesArr[this.state.order[this.state.quoteCount]].quote

it tries to get 0th element of an empty order array (which doesn’t exist, so it’s undefined).

2 Likes

@Sylvant: thank you, but that kind of approach potentially leads to repeating quotes before every quote has shown up, and I was trying to avoid that.

Also quoteCount is zero-based. The idea is to reinitialize quoteCount once it is equal to quotesArr.length, but the issue I’m having is not related to that. Thank you though.

@jenovs: order is indeed initialized as an empty array, but then, in componentDidMount is filled with as many indexes as needed (i.e., the number of quotes in quotesArr).

It is probably a bit convoluted, but that is what I came up with so far.

oh, now i see what you meant

I’ll try to narrow down my help request.
Given an array of objects, which contains a quote and an author:

let quotesArr = [
	{	
		quote: "...",
		author: "..."
	},
	{
		quote: "...",
		author: "..."
	}
	...
];

and the following react component QuoteBox:

class QuoteBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
	  //for sake of semplicity, let's have the order array already filled with shuffled indexes:
      order: [2, 7, 11, 20, 1, 15, 19, 3, 8, 13, 0, 5, 12, 10, 9, 18, 16, 4, 6, 14],
      //quoteCount increments every time user presses 'new-quote'  button...
      quoteCount: 0
    }
    this.getNewQuote = this.getNewQuote.bind(this);
  }

//I though I could access the quotes in the following fashion(in the render() method):
let quote = quotesArr[this.state.order[this.state.quoteCount]].quote;
//but it doesn't work:(quotesArr[this.state.order[this.state.quoteCount]].quote is undefined)

//note that the following works:
let quote = quotesArr[this.state.quoteCount].quote;

//and that, if I do this inside the return of the render() method:
<p>current quote is number: {this.state.order[this.state.quoteCount]}</p>

//the p element does display the index I want to use for accessing the array.

So, I don’t really understand why this code it’s not working.
But I Hope I have cleared a bit the issue.

Hi @Marco16168 ,

As @jenovs has already said, the initial render will have order= and quoteCount=0 which will throw an error at
quotesArr[this.state.order[this.state.quoteCount]].quote, as the index is undefined.

You can verify this by logging the value of this.state.order , this.state.quoteCount and [this.state.order[this.state.quoteCount]] in the console.

1 Like

Render comes before mount.

1 Like

Hey Marco,

Your method and logic are all working just fine, But the order in which react calls lifecycle methods is a little surprising.

The order is:
constructor → render → componentDidMount → render.

So that initial render call before the componentDidMount is what is causing the hang up. When that render method is called this.state.order is still an empty array.

I think this page might be help explain what is happening.
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

And here is a quote from the React docs:
" You may call setState() immediately in componentDidMount() . It will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state "

As for a solution the quickest fix that I can think of is initializing the array with an initial element. If I am guessing correctly it wouldn’t even need to be anything meaningful, Then ensure that during componentDidMount that initial element is overwritten. then during that re-render you should see a random first quote.

Let me know if it works!

1 Like

Thank you, this is what I was missing. Thinking better about it, it is clear now that componentDidMount takes place after a first rendering of the DOM. I was somehow convinced that it took place before rendering.
Also thank you for the link, very useful.
I tried to initialize the state in the QuoteBox like so:

this.state = { 
	order: [0],
	quoteCount: 0
}

and it works now.

May I ask you one more thing, if I need a function to be reachable both in the class constructor and in its methods (e.g., componentDidMount or other custom methods), where would I need to define it in the code?

Now anyone on here can feel free to correct me if I am wrong, or if something that I say defies some best practice.

If you are trying to use a function in a constructor, you can totally do that, provided that function ‘exists’, i.e. has been defined before you are trying to use it.

For example, if we are talking about your quote machine, The below code should work.
When React calls the constructor method the function shuffleIdx has already been defined as a global JS function. So state.order will be initially set to a random order. Then you don’t need use componentDidMount at all.

//  This way shuffleIdxs is a globally defined function. Becuase it is defined before and
 // outside of the class, any class that you define after can use that function. 
function shuffleIdxs() {
      let order = [];
      for (let i = 0; i < quotesArr.length; i++) { order.push(i) };
      for (let i = order.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        [order[i], order[j]] = [order[j], order[i]];
      }
      return order;
    }

class QuoteBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      order: shuffldxs(),
      quoteCount: 0
    }
    this.getNewQuote = this.getNewQuote.bind(this);
  }
  getNewQuote() {
    this.setState(state => ({
      quoteCount: state.quoteCount + 1
    }))
  }
  render() {

if you want to avoid the global function, you should be able to define the function in the class constructor (provided you use a function declaration 'function myFunction() {…} and not a function expression ‘let myFunction = () => {…}’ ).
The following code should work, and it is a more general example:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: randomNumber(),
    };
    function randomNumber() {
      return Math.random();
    }
  }
  render() {
    return <h1>{this.state.number}</h1>;
  }
}

The tricky bit comes in when you have complicated functions that take a while to run. If you are initializing a state variable during the constructor method using a function. And then that function takes a while to run, react isn’t going to be able to call render() until that function has finished running, i.e. the component cant render.

Did that answer the question? I think it is helpful to remember that at its core we are just working JavaScript classes. When you declare a react class component you are making a JavaScript class that is a child of the class React.Component.

1 Like

Yes, definitely. Thank you, you’ve been very helpful.
I think I tried to define the function inside the class constructor, but I did that using a function expression, and that’s probably why it didn’t work.

Yes, besides the JavaScript - Object Oriented Programming bit here at fCC I’m not very familiar with classes, I should probably study them better.

Have a good day!

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.