How to run a function only once in react

Viewed 2056

I'm working on a simple quiz application. I have a local array with questions that is in randomised order each time. The function that is shuffling the array in different order is triggered each time the handleAnswerOptionClick is run.

How can I run the arrayShuffle function once?

My guess is that I could use useEffect for this but I haven't gotten it to work. It complains about that it doesn't have different dependencies.

 useEffect(() => {
    arrayShuffle(questions);
  }, []);

Code: CodeSandox

QuizContainer:

import React, { useState, useEffect } from "react";

export const QuizContainer = () => {
  const questions = [
    {
      questionText: "What is the capital of France?",
      answerOptions: [
        { option: "New York", isCorrect: false },
        { option: "London", isCorrect: false },
        { option: "Paris", isCorrect: true },
        { option: "Dublin", isCorrect: false }
      ]
    },
    {
      questionText: "Who is CEO of Tesla?",
      answerOptions: [
        { option: "Jeff Bezos", isCorrect: false },
        { option: "Elon Musk", isCorrect: true },
        { option: "Bill Gates", isCorrect: false },
        { option: "Tony Stark", isCorrect: false }
      ]
    },
    {
      questionText: "The iPhone was created by which company?",
      answerOptions: [
        { option: "Apple", isCorrect: true },
        { option: "Intel", isCorrect: false },
        { option: "Amazon", isCorrect: false },
        { option: "Microsoft", isCorrect: false }
      ]
    },
    {
      questionText: "How many Harry Potter books are there?",
      answerOptions: [
        { option: "1", isCorrect: false },
        { option: "4", isCorrect: false },
        { option: "6", isCorrect: false },
        { option: "7", isCorrect: true }
      ]
    }
  ];

  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [showScore, setShowScore] = useState(false);
  const [score, setScore] = useState(0);

  // Event handlers
  const handleAnswerOptionClick = (isCorrect) => {
    if (isCorrect) {
      setScore(score + 1);
    }

    const nextQuestion = currentQuestion + 1;
    nextQuestion < questions.length
      ? setCurrentQuestion(nextQuestion)
      : setShowScore(true);
  };

  // ShuffleArray
  const arrayShuffle = function (arr) {
    let newPos, temp;
    for (let i = arr.length - 1; i > 0; i--) {
      newPos = Math.floor(Math.random() * (i + 1));
      temp = arr[i];
      arr[i] = arr[newPos];
      arr[newPos] = temp;
    }
    return arr;
  };

  const newArray = arrayShuffle(questions);
  console.log(newArray);

  return (
    <>
      <h1>QuizContainer</h1>
      {showScore ? (
        <p>
          You scored {score} out of {questions.length}
        </p>
      ) : (
        <>
          <p>
            Question {currentQuestion + 1}/{questions.length}
          </p>

          <p>{questions[currentQuestion].questionText}</p>

          <div>
            {questions[currentQuestion].answerOptions.map(
              (answerOption, index) => (
                <div key={index}>
                  <button
                    onClick={() =>
                      handleAnswerOptionClick(answerOption.isCorrect)
                    }
                  >
                    {answerOption.option}
                  </button>
                </div>
              )
            )}
          </div>
        </>
      )}
    </>
  );
};

Just ask if I need to clarify and if you guys think that I'm approaching this the right way.

Thanks beforehand,

Erik

1 Answers

Two things:

  1. If you want a function to run on mount you use useEffect with an empty dependency array and you may safely ignore the linter warnings in that case and that case only.
  2. That isn't (as far as I can tell) what you want: you want to run it once when the page loads and then never again.

If that is the case then there's no need to even put it in a component. Pull it out into it's own top-level thing:

  const questions = [
    {
      questionText: "What is the capital of France?",
      answerOptions: [
        { option: "New York", isCorrect: false },
        { option: "London", isCorrect: false },
        { option: "Paris", isCorrect: true },
        { option: "Dublin", isCorrect: false }
      ]
    },
    {
      questionText: "Who is CEO of Tesla?",
      answerOptions: [
        { option: "Jeff Bezos", isCorrect: false },
        { option: "Elon Musk", isCorrect: true },
        { option: "Bill Gates", isCorrect: false },
        { option: "Tony Stark", isCorrect: false }
      ]
    },
    {
      questionText: "The iPhone was created by which company?",
      answerOptions: [
        { option: "Apple", isCorrect: true },
        { option: "Intel", isCorrect: false },
        { option: "Amazon", isCorrect: false },
        { option: "Microsoft", isCorrect: false }
      ]
    },
    {
      questionText: "How many Harry Potter books are there?",
      answerOptions: [
        { option: "1", isCorrect: false },
        { option: "4", isCorrect: false },
        { option: "6", isCorrect: false },
        { option: "7", isCorrect: true }
      ]
    }
  ];

  const arrayShuffle = function (arr) {
    let newPos, temp;
    for (let i = arr.length - 1; i > 0; i--) {
      newPos = Math.floor(Math.random() * (i + 1));
      temp = arr[i];
      arr[i] = arr[newPos];
      arr[newPos] = temp;
    }
    return arr;
  };

  const newArray = arrayShuffle(questions);

  export const QuizContainer = () => {
    /* rest of component */
  }

If you do want to re-run it but want to have control over doing so you can put it in state:

export const QuizContainer = () => {
  const [shuffled, setShuffled] = useState(arrayShuffle(questions));
  const reShuffle = () => setShuffled(arrayShuffle(questions));
  /* rest of the component */
}

But in general don't clutter your component definitions with static constants that will remain unchanged for the entire pageview.

Related