Build A Simple Quiz App, with React Hooks & Typescript
Tags: tutorial, quiz, react, typescript
Published: 2020-05-03 14:10:19

Let's build a quiz app using React Hooks and Typescript. This little application will help us learn how to use React Hooks and Typescript in progressively complex ways. Until using them comes easily and finally 'hooks' into our brains.

So let's get in there: &#x1f9e0; &#x1f9e0; &#x1f9e0;

##What are React Hooks?
[React Hooks](https://reactjs.org/docs/hooks-intro.html) were stabilized and shipped with [React 16.8](https://reactjs.org/docs/getting-started.html). Without Hooks, React Functional Components would not have a means to use React Lifecycle methods (RLMs), among other things. And instead, you'd have to use Class Components to use the RLMs. 

But React Hooks right that ship. With them you can 'hook' into [React Lifecycle Methods](https://reactjs.org/docs/state-and-lifecycle.html), manage your application state, and even create reducers on the fly without having to use heavier (but equally great, and sometimes preferable) state-management libraries such as Redux.

##Why add Typescript?
JavaScript is a **weakly typed** language, which means that on variable declaration you aren't required to declare its type, and JavaScript will change the type of a variable as your whims change. For example, here's something we may try (if we had no idea what we were doing, perhaps? Been there!): 
```javascript
let number = '22' // variable is a string
number = {
  age: 22,
  birthdayYear: 2020 
}
console.log(number) // variable is now an object { age: 22, birthdayYear: 2020 }
```
JavaScript just shrugs... ¯\_(ツ)_/¯ This loosey-goosey behavior can, overall, make applications buggier and far harder to maintain. Typescript stops this madness. As a superset of JavaScript, Typescript evaluates your code during compile-time (as opposed to run-time) to be sure your variables and props are what you intend them to be. So the above will turn into this:
```javascript
let number: string = '22' // variable is a string
number = {
  age: 22,
  birthdayYear: 2020 
} // Type '{ age: number; birthdayYear: number; }' is not assignable to type 'string'.
```
Typescript says, oh heck NO! &#x1F631; And thank goodness it does! It may seem like a chore to add Typescript and type declarations on smaller applications, but as you build larger applications and work with more engineers, it will go a long way to writing cleaner, more-testable code. So now that we know *why* it may be good to use React Hooks and Typescript, let's start writing some code!

##Setting up the app

I wont go into great detail about setup, because that's the easy part. But easiest route is to use [Create React App (CRA)] (https://reactjs.org/docs/create-a-new-react-app.html); another resource: [CRA Github page](https://github.com/facebook/create-react-app). For CRA, in your terminal, type this:
```javascript
npx create-react-app my-app
cd my-app
```
Now install typescript and the other packages you may need if you'll also use Lodash, Jest, or Sass:
```javascript
npm install --save typescript @types/jest @types/lodash @types/node @types/node-sass @types/react @types/react-dom lodash node-sass react-dom
npm install // to install all the packages in package.json file 
```
Then type
```javascript
npm start // to run the app at http://localhost:3000
```
To officially change the app to Typescript, change the `.js` files in which you'll use JSX into `.tsx` files. Any other `.js` file can become a .`ts` file.
```javascript
// src/components/App.tsx
import React, { FunctionComponent } from 'react';

import MainContainer from './Main';

const App: FunctionComponent = () => {
  return (
    <div>
      <MainContainer />
    </div>
  );
};

export default App;
```
The `FunctionComponent` type declaration is a Typescript typing for a functional component. Previously, you'd type it as a `StatelessComponent`, but that is now **deprecated** because technically any functional component can now have state.

This next component is the MainContainer. Here we'll import `useEffect, useState` from React to start using our Hooks. In the below file, you'll see `const MainContainer: FunctionComponent<{ initial?: Models.QuizResponse; }> = ({ initial })`. This sets the type of the `FunctionComponent` and also declares the data type we expect back from the data `fetch` to opentdb.com: `Models.QuizResponse`. 

`useEffect` is a React hook that allows side effects in function components and enables access to the React Lifecycle Methods (RLMs) `componenDidMount(), componentDidUpdate(),` and `componentWillUnmount()` all in one. See more about the [React useEffect() Hook](https://reactjs.org/docs/hooks-effect.html) in the docs.
 
```javascript
// src/components/MainContainer.tsx
import React, { FunctionComponent, useEffect, useState } from 'react';

import * as Models from './../models';
import Card from './Card';
import './Main.scss';

const MainContainer: FunctionComponent<{ initial?: Models.QuizResponse; }> = ({ initial }) => {
  // Below is one way state is set using React Hooks, where the first deconstructed variable`quizzes` is the state variable name 
  // and `setQuizzes` is the methodName called to update the quizzes state if needed. Here, use it after the data is fetched successfully. 
  const [quizzes, setQuizzes] = useState(initial);
  const [shouldShowCards, setShouldShowCards] = useState(false);

  const fetchData = async (): Promise<void> => {
    const res = await fetch('https://opentdb.com/api.php?amount=10&type=boolean');
    res.json()
      .then((res) => setQuizzes(res))
      .catch((err) => console.log(err));
  };

  // useEffect is a React hook that allows side effects in function components and enables the React Lifecycle Method (RLM)
 // componenDidMount(), componentDidUpdate(), and componentWillUnmount() lifecycles combined. See more about 
 // the [React useEffect() Hook](https://reactjs.org/docs/hooks-effect.html) in the docs.
  useEffect(() => {
    fetchData();
  }, []);

  const handleButtonClick = (): void => {
    setShouldShowCards(true);
  };

  return (
    <main className='Main'>
      {!shouldShowCards ? (
        <>
          <h2>Welcome to the Trivia Challenge!</h2>
          <div className='StartEndCard'>
            <h2>You will answer 10 of the most rando true or false questions</h2>
            <p>Can you score 10/10?</p>

            <button type='submit' className='Button' onClick={() => handleButtonClick()}>Get Started!</button>
          </div>
        </>
      ) : <Card quizQuestions={quizzes} />}
    </main>
  );
};

export default MainContainer;
```
Here is how I've set up the Typescript models for this quiz app: 

```javascript
// src/models/Quiz.ts - not a TSX file, because there is no JSX used here. We'll store all TS models called models or similar.
export type Quiz = {
  category: string;
  type: string;
  difficulty: string;
  question: string;
  correct_answer: string;
  incorrect_answers: [
    string
  ];
};

export type QuizResponse = {
  response_code: number;
  results: Quiz[];
}
```
And here's where a lot of the magic happens. The Card component uses Hooks in a different way than the MainComponent, because the Card component has more complexity. You'd be declaring variables for days follwing the previous page's pattern. Instead, just create an `initialState`, similarly to how you would on a [React Class component](https://reactjs.org/docs/components-and-props.html). 

And that'll also make the state easier to reset when the user has finished the quiz. On button click, we just pass in the initial state to our setState({}) method. 
// src/components/Card.tsx
```javascript
import _ from 'lodash';
import React, { useState } from 'react';

import * as Models from './../models';
import './Card.scss';

interface Props {
  quizQuestions?: Models.QuizResponse;
}

const Card = (props: Props) => {
  const quizQuestions = _.get(props, 'quizQuestions.results', []);
  // Here is another way to set state using React Hooks. This is a neater approach than setting them individually like you'll see
  // in Main.tsx. This approach is great for larger states.
  const initialState = {
    currentIndex: 0,
    score: 0,
    showFinished: false,
    answered: false,
    selectedOption: '',
    revealAnswer: '',
  };

  // These two variable below can be called anything, but we'll name them `state` and `setState` for convention.
  const [state, setState] = useState(initialState);
  // These are variables that we'll refer to throughout this component, so we'll set them on state here. If there are variables you
  // are not referring to outside of the setState({}) funciton elsewhere, they dont need to be delcared here, but can be just set above.
  const {
    currentIndex,
    score,
    revealAnswer,
    selectedOption,
  } = state;
   
   // A click event is typed as React.ChangeEvent<HTMLInputElement>
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>, correctAnswer: Models.Quiz): void => {
    e.persist();
    e.preventDefault();
    const isCorrect: boolean = e.target.id.includes(correctAnswer.toString()) ? true : false;
    const renderAnswer: string = isCorrect ? 'Correct!' : 'Sorry, wrong answer!';

    setState({
      ...state,
      selectedOption: e.target.id.toString(),
      answered: isCorrect ? true : false,
      revealAnswer: renderAnswer
    });

    if (currentIndex + 1 > quizQuestions.length) {
      setState({ ...state, showFinished: true });
    } else {
      // delay for question auto-advance, to display 'Correct' or 'Incorrect' feedback
      setTimeout(() => {
        setState({ ...state, score: isCorrect ? score + 1 : score + 0, currentIndex: currentIndex + 1, revealAnswer: '' });
      }, 2000);
    }
  };
  // Below you could return a div, but since we already have an anchor div below, let's return a fragment.
  const renderAnswer = (): React.ReactFragment => {
    return (
      <>{revealAnswer}</>
    );
  };

  return (
    quizQuestions && quizQuestions.length > 0 && (currentIndex < quizQuestions.length) ?
      <div>
        <h2>{quizQuestions[currentIndex].category}</h2>
        <main className='Card'>
          <h1>{_.unescape(quizQuestions[currentIndex].question)}</h1>
          <div>Difficulty: {quizQuestions[currentIndex].difficulty}</div>
        </main>

        <section>
          <div className='Answer'>{renderAnswer()}</div>

          <form className='form'>
            <div className='inputGroup' role='radiogroup'>
              <label id='label' htmlFor='radioTrue' className='container'><input id='radioTrue' name='radio' type='radio' checked={selectedOption === 'True'} onChange={(e) => handleChange(e, quizQuestions[currentIndex].correct_answer)} />
              True<span className='checkmark'></span></label>
            </div>
            <div className='inputGroup' role='radiogroup'>
              <label id='label' htmlFor='radioFalse' className='container'><input id='radioFalse' name='radio' type='radio' checked={selectedOption === 'False'} onChange={(e) => handleChange(e, quizQuestions[currentIndex].correct_answer)} />
              False<span className='checkmark'></span></label>
            </div>
          </form>
        </section>

        <footer className='Badge'>
          Question {currentIndex + 1}/{quizQuestions.length}
        </footer>
      </div>
      :
      <div>
        <main className='Card'>
          <h3>
            You scored {score} / {quizQuestions.length}
          </h3>

          <button className='Button' type='reset' onClick={() => setState(initialState)}>
            Start Over
          </button>
        </main >
      </div>
  );
};

export default Card;
```

Lastly there some Sass files just to make things look good, but you dont need to follow those if you want to achieve your own aesthetic. 

Checkout the full application on my [Cat Perry Github page](https://github.com/CatPerry/quiz-app-using-react-hooks-and-typescript).

If you've found this helpful. Please [Share it on Twitter](https://twitter.com) and heart it as well. Happy coding, happy passing it on!