Learn React basics by creating a simple calculation game (Part 2)

Dec 29, 2020 路 React, useEffect, simple calculation game 路 5 minute read

This is part 2 of a series of blog posts where I explore basic React principles by building a simple calculation game. In this blog post I will improve the app I created in Part 1. You might want to read that first, so you know what the app is about and how it was built.

In this blog post we'll make the questions dynamic, because answering the same question over and over again is a bit boring.

The main React topic we'll cover in this blog post is the useEffect hook.

Demo

screenshot of the simple calculation game

Or check out the demo on Netlify. Version 1 we built in part 1 and now we will create version 2.

A function for random questions

We need a function that returns a random question. This question consists of an answer, which is the number you'll have to add up to, and choices, which is an array of numbers to choose from.

The function will return an object containing these two properties. To keep it simple, the choices array will still be hard-coded and answer will be a random number between 2 and 10:

function getQuestion() {
  const min = 2;
  const max = 10;
  const answer = Math.floor(Math.random() * (max - min + 1) + min);
  return { answer, choices: [1, 2, 3, 4] };
}

In our App component we'll need to call this function. But before we do that we'll have to answer the following question: Does this function need to be inside or outside the component?

I always keep these kind of functions outside the component.

The first reason for that is about separation of concern: A React component is responsible for rendering UIs and should contain all code that is needed to do that. However, the getQuestion function has nothing to do with UIs, it's a function that contains business logic and even data. Of course it will be used for the UI in this case, but it could also be used for another purpose. Putting it outside keeps the component code clean.

A second reason is about the behavior of React components. With every state change (for example: when clicking the numbers) the App component will be re-rendered which means the getQuestion function would be re-created with every render. For this app this is not really a big deal, but it is unnecessary.

Having said that, for this app, there is nothing wrong with putting the function inside the component if that is your personal preference, but I do not recommend it.

The second question is: Should I put this function in the App component file or in a separate file and then import it? The answer to this question is purely a personal preference. I prefer a separate file and import it, again, to keep the App component file clean.

Calling the getQuestion function

Before calling getQuestion, we'll refactor our component a bit. Until now, our component started with the following:

function App() {
  const answer = 3;
  const choices = [1, 2, 3, 4];
  const [selected, setSelected] = useState([]);
  const [result, setResult] = useState(null);

As said before, answer and choices will be merged into one object which will be returned by the getQuestion component. This question object changes with each call to getQuestion and should be displayed, which means it becomes stateful:

function App() {
  const [question, setQuestion] = useState({ choices: [] });
  const { answer, choices } = question;
  const [selected, setSelected] = useState([]);
  const [result, setResult] = useState(null);

I did two things in this code to make sure the rest of the component's code won't break because of this change, which are worth noting:

First of all, the useState call for question gets an initial state with an empty choices array passed in. This is to make sure the choices.map call in the JSX keeps on working on the first render when getQuestion has not been called yet. An alternative fix is to not provide an initial state and check for the existence of the choices array before mapping it. But that means unnecessary clutter in the JSX and I don't like that.

The second thing I did, which is also optional, is on the second line where I destruct answer and choices from question. This is to prevent I have to prepend all usages of these variables throughout the component with question..

Now let's fi-nal-ly call getQuestion in our component...

Both when the component is rendered for the first time and after answering a question, the getQuestion function should be called so the UI will display a new question. This is a side effect as there is no direct user interaction involved that triggers getting a new question. For that we'll call React's useEffect hook inside our component, which in turn calls our getQuestion function:

useEffect(() => {
  const newQuestion = getQuestion();
});

Next we need to update the state of question by calling setQuestion:

useEffect(() => {
  const newQuestion = getQuestion();
  setQuestion(newQuestion);
}, []);

Note how I also added the empty dependency array to prevent the notorious infinite useEffect loop.

What happens now is that when the component is rendered for the first time, useState is called and the choices array is empty. The JSX is rendered and nothing is displayed. However, directly after that, useEffect is called, which gets a question, updates the question state and the question is displayed. This is how React works and it happens so fast the user won't notice it.

After answering the question, getQuestion should be called again, but this is not happening. The reason for that is the empty dependency array we passed to useEffect, which means the code is only called after the first render.

The question is when (and how) should getQuestion be called again? We'll use the already existing stateful result variable for that. When an answer is given, result will get a value indicating the answer is correct or not. After clicking Try Again, result is cleared again, which happens in the reset function. So what our useEffect code should do is check for the result variable: a new question should be retrieved when result is empty:

useEffect(() => {
  if (!result) setQuestion(getQuestion());
}, [result]);

Note how I also added result to the dependency array. Our side effect code is dependent on that value as it needs it to determine whether getQuestion should be called or not.

Wrapping up

This is quite a long story for quite a small adjustment to our code. However, it's a significant one: The app now gives you infinite pleasure of trying to add up to numbers between 2 and 10! 馃槈

By the way, did you notice we only changed the behavior of our component and not the UI (JSX)? This shows how nice and clean behavior and UI can be separated in a React component.

In my next blog post, which is part 3 already, I will cover the useReducer hook, which we'll use to make the component's code even cleaner.

P.S. The source code for this app is on my GitHub: https://github.com/bouwe77/react-simple-calculation-game/blob/main/src/simple-calculation-game/v2

Share on TwitterDiscuss on TwitterEdit on GitHub