Building English Quiz App - My First SolidJS App as a React Developer
Tags: solidjs, tutorial, react, webdev
Published: 2023-06-11 18:40:45

> My First SolidJS App: English Quiz App as a React Developer

I have primarily focused on React projects for the past three years. When I initially made the decision to learn a frontend framework, the top three frameworks were `React`, `Vue` and `Angular`. Out of these options, I opted for `React`. Since then, I haven't tried learning other frameworks. 

About three months ago, I arrived in Frankfurt. I am currently studying English by myself to pursue job opportunities here. Prior to diving into employment, I realized that this might be a good opportune moment to try a new frontend framework.

---

## Why SolidJS

[SolidJS](https://www.solidjs.com/): Simple and performant reactivity for building user interfaces.

If you have ever prepared for a frontend interview, especially, for a React Developer, you may have come across the question. 'What is the VirtualDOM?'.

In `React`, the **VirtualDOM** resides in the memory and serves as a representation of the **RealDOM**. By comparing the VirtualDOM with the **RealDOM**, React identifies the specific portions of the **RealDOM** that requires updating and it comes improving performance.

However, I was amazed to discover that both `Svelte` and `SolidJS` which are getting popular these days boast even better performance than `React`, and they accomplish this without the need for the **VirtualDOM**.

Intrigued by this information, I decided to give one of them a try. After some research, I noticed that `SolidJS` bears a striking resemblance to `React`. This led me to believe that `SolidJS` might have a lower learning curve for me compared to `Svelte`.

---

Initially, I had considered building **a Todo app**, which is a common choice for many people as their first frontend toy project. However, I found the idea to be somewhat dull, and I was certain that my interest in the app would be gone when I finish it. Instead, I decided to create **an English quiz app**. This alternative seemed more enjoyable, and since I would personally use it for studying purposes, it would be more useful to me as well.

In this article, I will walk you through the code of the app page by page. The explanation may not be entirely smooth, as I won't be following a step-by-step approach. I aim to cover everything I have learned about `SolidJS` and occasionally draw comparisons with `React`. If you come across any errors or have suggestions, please feel free to comment down below or submit pull requests on [this Github repository](https://github.com/hsk-kr/solid-english-quiz). 

Lastly, please, forget about the design. I need to improve my skills in it.

---


![Intro Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pudzj3uaq5x4uhv9lbp2.png)


![Quiz Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9bjz1nuzdne76qqxpmi.png)


![Result Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m6tuecqgh4752re6r4gg.png)


![Intro Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/or5qwn1hg6p3kkei22x8.png)

[https://solid-english-quiz.vercel.app](https://solid-english-quiz.vercel.app/) as a vercel domain

[Github Repository](https://github.com/hsk-kr/solid-english-quiz)

In the Github repository associated with this article, there is a separate branch called **'devto'**. This branch is specifically created for the purpose of this post. As I continue to maintain the repository, the **'main'** branch might diverge from the code presented in this article.


---

## Set Up The Project

1\. Create a `SolidJS` project

```bash
> npx degit solidjs/templates/ts english-quiz
```

2\. Install packages with your prefer package manager

```bash
> pnpm install
```

I added two more packages.

- [SolidJS Router](https://github.com/solidjs/solid-router): A universal router for SolidJS 
- [Tailwind](https://tailwindcss.com/): A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.

3\. Install SolidJS Router

```bash
> pnpm install @solidjs/router
```

4\. Install Tailwind

```bash
> pnpm install -D tailwindcss postcss autoprefixer
```

5\. Create a config file for tailwind

```bash
> npx tailwindcss init -p
```

6\. Change **tailwind.config.js**

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      height: {
        screenUI: "100svh", // For mobile browsers
      },
    },
    // Extend Font
    fontFamily: {
      caveat: ["Caveat"],
      roboto: ["Roboto"],
    },
  },
  plugins: [],
};
```

I have added two google fonts to the project and a custom height style in tailwind, which reflects the smallest possible viewport height.

7\. Change **index.html**

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
    />
    <meta name="theme-color" content="#000000" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
      rel="stylesheet"
    />
    <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
    <title>English Quiz</title>
    <meta
      name="description"
      content="Test your English and practice English by solving quiz"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>

    <script src="/src/index.tsx" type="module"></script>
  </body>
</html>
```

I have added link tags to apply the Google fonts and included some metadata such as **description**. Additionally, I have modified the content attribute of the viewport meta tag to prevent scaling, as this can lead to an uncomfortable user experience on mobile devices.

8\. Change **index.css**

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  font-size: 110%;
}

body {
  margin: 0;
  touch-action: pan-x pan-y;
}
```

In order apply the Tailwind Css styles, I have included the lines **@tailwind ~** in the file. `font-size` property is used to specify the desired gap between the two fonts, and the `touch-action` property is utilized to prevent scaling on certain mobile browsers, such as Safari.

---

## Router & App

1\. Create **src/MainRouter.tsx**

```typescript
import { Route, Router, Routes } from "@solidjs/router";
import Intro from "./pages/Intro";
import Quiz from "./pages/Quiz";
import NotFound from "./pages/NotFound";
import Result from "./pages/Result";

const MainRouter = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" component={Intro} />
        <Route path="/quiz" component={Quiz} />
        <Route path="/result" component={Result} />
        <Route path="*" component={NotFound} />
      </Routes>
    </Router>
  );
};

export default MainRouter;
```

Before finalizing the entire code of the project, please be aware that there may be some errors present. If you plan to run the development server at any point, I recommend commenting out the lines that are causing errors. This will allow you to proceed with running the server without encountering any issues.

2\. Create **src/components/templates/index.tsx**

```typescript
import { Component, JSXElement } from "solid-js";
import fontSignal from "../../../stores/fontSignal";

const BaseTemplate: Component<{
  children?: JSXElement;
}> = (props) => {
  const [font] = fontSignal;

  return (
    <div class={`w-screen h-screenUI bg-lime-50 ${font()}`}>
      {props.children}
    </div>
  );
};

export default BaseTemplate;
```

The BaseTemplate component serves as a way to provide a default style to its child components. It applies a specific font and utilizes the **h-screenUI** class, which is a custom height class defined in the Tailwind CSS configuration file. By using this component, we can ensure consistency in the style across all pages of our application.

3\. Create **src/stores/fontSignal.tsx**

```typescript
import { Accessor, Setter, createSignal, createEffect } from "solid-js";

type Font = "font-caveat" | "font-roboto";

const defaultFont: Font = "font-caveat";
const STORAGE_FONT = "STORAGE_FONT";

const fontSignal = ((): [Accessor<Font>, Setter<Font>, VoidFunction] => {
  const initFont: Font =
    (localStorage.getItem(STORAGE_FONT) as Font) || defaultFont;
  const [font, setFont] = createSignal<Font>(initFont);

  const nextFont = () => {
    let newFont: Font = "font-caveat";

    switch (font()) {
      case "font-caveat":
        newFont = "font-roboto";
        break;
    }

    localStorage.setItem(STORAGE_FONT, newFont);
    setFont(newFont);
  };

  createEffect(() => {
    const rootElement = document.querySelector<HTMLElement>(":root");

    switch (font()) {
      case "font-caveat":
        rootElement?.style.setProperty("font-size", "110%");
        break;
      case "font-roboto":
        rootElement?.style.setProperty("font-size", "100%");
        break;
    }
  });

  return [font, setFont, nextFont];
})();

export default fontSignal;
```

I struggled to decide on a suitable name for the directory, and eventually settled on **'stores'**. However, if you have alternative suggestions, feel free to use them.

The `createSignal` function in SolidJS is comparable to the `useState` hook in React. However, unlike `useState`, `createSignal` can be used anywhere, not just within components.

Similarly, `createEffect` in SolidJS is similar to `useEffect` in React. In the code, you may have noticed that there are no dependencies specified. When the `font()` function is called, the getter function remembers where it was called from, then when you change the font using the font setter, the `createEffect` will be triggered.

The `font-caveat` class represents a slightly smaller font size compared to `font-roboto`, that is why I gave a difference in font size between the two.

The `nextFont` function is used to switch between fonts.

To ensure that the latest font selected by users is preserved, the data is stored in the local storage. This allows the application to apply the font even after a page reload.

---

**[+Edit] Since `create*` doesn't matter where it's defined at any scope, you can write the code like below.**

```typescript
import { createSignal, createEffect } from "solid-js";

type Font = "font-caveat" | "font-roboto";

const defaultFont: Font = "font-caveat";
const STORAGE_FONT = "STORAGE_FONT";

const initFont: Font =
  (localStorage.getItem(STORAGE_FONT) as Font) || defaultFont;
const [font, setFont] = createSignal<Font>(initFont);

const nextFont = () => {
  let newFont: Font = "font-caveat";

  switch (font()) {
    case "font-caveat":
      newFont = "font-roboto";
      break;
  }

  localStorage.setItem(STORAGE_FONT, newFont);
  setFont(newFont);
};

createEffect(() => {
  const rootElement = document.querySelector<HTMLElement>(":root");

  switch (font()) {
    case "font-caveat":
      rootElement?.style.setProperty("font-size", "110%");
      break;
    case "font-roboto":
      rootElement?.style.setProperty("font-size", "100%");
      break;
  }
});

export { font, setFont, nextFont };

export default {
  font,
  setFont,
  nextFont,
};
```

---

4\. Create **src/pages/NotFound.tsx**

```typescript
import BaseTemplate from "../../components/templates/BaseTemplate";

const NotFound = () => {
  return (
    <BaseTemplate>
      <div
        class="mx-auto flex flex-col w-fit text-center gap-y-4"
        style={{ "padding-top": "20%" }}
      >
        <h1 class="text-8xl">404</h1>
        <span class="text-3xl">Page Not Found</span>
        <a href="/" class="text-lg text-gray-800">
          GO HOME
        </a>
      </div>
    </BaseTemplate>
  );
};

export default NotFound;
```

This page will be displayed when users attempt to access a page that does not exist.

5\. Change **src/App.tsx**

```typescript
import type { Component } from "solid-js";
import MainRouter from "./MainRouter";
import PreventDoubleTab from "./components/functions/PreventToDoubleTab";

const App: Component = () => {
  return (
    <>
      <PreventDoubleTab />
      <MainRouter />
    </>
  );
};

export default App;
```

6\. Create **src/components/functions/PreventDoubleTab**

```typescript
import { onCleanup, onMount } from "solid-js";

const PreventDoubleTab = () => {
  onMount(() => {
    const preventDefault = (e: MouseEvent) => {
      e.preventDefault();
    };
    document.addEventListener("dblclick", preventDefault);

    onCleanup(() => {
      document.removeEventListener("dblclick", preventDefault);
    });
  });

  return null;
};

export default PreventDoubleTab;
```

I encountered an issue where double-tapping on my iPhone SE2 caused the page to zoom in, despite setting `user-scalable=no` in the viewport meta tag. To address this problem, I implemented this component that prevents zooming in with a double-tap gesture.

In this component, I utilized the `onMount` function. Even if you don't use `onMount`, the code will still execute the code once, however it guarantees that the code will not run during server-side rendering (SSR). You can find more information about `onMount` in the SolidJS tutorial [here](https://www.solidjs.com/tutorial/lifecycles_onmount).

Addtionally, I utilized the `onCleanup` function, which is used to release resources. You can call it at any scope.

---

### Intro Page

1\. Create **src/types/quiz.ts**

```typescript
export interface Quiz {
  quizName: string;
  quizList: {
    answer: string;
    question: string;
  }[];
}

export interface PlayHistory {
  [key: string]: number;
}
```

2\. Create **src/data/quizzes.ts**

```typescript
import { Quiz } from "../types/quiz";

const a1Words: Quiz = {
  quizName: "A1 English Words",
  quizList: [
    {
      answer: "hello",
      question: "A common greeting used to acknowledge or greet someone.",
    },
    {
      answer: "goodbye",
      question:
        "A parting phrase used to bid farewell or take leave of someone.",
    },
    {
      answer: "yes",
      question: "An affirmative response or agreement.",
    },
    {
      answer: "no",
      question: "A negative response or denial.",
    },
    {
      answer: "thank you",
      question:
        "An expression of gratitude or appreciation for something received or done.",
    },
    {
      answer: "please",
      question:
        "A polite word used to make a request or express a desire for something.",
    },
    {
      answer: "sorry",
      question:
        "An expression used to apologize or show regret for a mistake or wrongdoing.",
    },
    {
      answer: "excuse me",
      question:
        "A polite phrase used to get someone's attention or ask for forgiveness.",
    },
    {
      answer: "how are you",
      question:
        "A common greeting used to ask about a person's well-being or current state.",
    },
    {
      answer: "my name is",
      question: "An introduction phrase used to state one's name.",
    },
  ],
};

const quizzes: Quiz[] = [
  a1Words,
];

export default quizzes;
```

I have generated the data using `ChatGPT`. You can utilize `ChatGPT` to generate more data.

3\. Create **src/pages/Intro/index.tsx**

```typescript
import { For, createSignal } from "solid-js";
import BaseTemplate from "../../components/templates/BaseTemplate";
import quizzesData from "../../data/quizzes";
import { useNavigate } from "@solidjs/router";
import KeyEvent, { Key } from "../../components/functions/KeyEvent";
import Button from "../../components/Button";
import fontSignal from "../../stores/fontSignal";
import { evalScore, getPlayHistory } from "../../lib/playHistory";
import Badge, { BadgeColor } from "../../components/Badge";

const Intro = () => {
  const [, , nextFont] = fontSignal;
  const [selectedQuizIdx, setSelectedQuizIdx] = createSignal(0);
  const navigate = useNavigate();
  const quizzes = () => {
    const quizzesListToShow = [];
    const startIdx = selectedQuizIdx();

    for (let i = startIdx - 1; i < startIdx + 2; i++) {
      if (i >= 0 && i < quizzesData.length) {
        quizzesListToShow.push(quizzesData[i]);
      } else {
        quizzesListToShow.push(null);
      }
    }

    return quizzesListToShow;
  };

  const nextArrowVisible = () => {
    return selectedQuizIdx() === 0 && quizzes().length >= 3;
  };

  const handleKeyEvent = (key: Key) => {
    switch (key) {
      case "Enter":
        const selectedQuiz = quizzes()[1];
        if (!selectedQuiz) break;
        navigateToQuiz(selectedQuiz.quizName);
        break;
      case "ArrowUp":
        setSelectedQuizIdx((prevSelctedQuizIdx) =>
          prevSelctedQuizIdx > 0 ? prevSelctedQuizIdx - 1 : prevSelctedQuizIdx
        );
        break;
      case "ArrowDown":
        setSelectedQuizIdx((prevSelctedQuizIdx) =>
          prevSelctedQuizIdx < quizzesData.length - 1
            ? prevSelctedQuizIdx + 1
            : prevSelctedQuizIdx
        );
        break;
    }
  };

  const navigateToQuiz = (quizName: string) => {
    navigate("/quiz", {
      state: {
        quizName,
      },
    });
  };

  return (
    <BaseTemplate>
      <KeyEvent
        onKeyUp={handleKeyEvent}
        keys={["Enter", "ArrowUp", "ArrowDown"]}
      />
      <div class="text-center h-full">
        <h1 class="text-5xl h-2/6 block flex items-end justify-center p-4">
          English Quiz
        </h1>
        <div class="mt-16 text-2xl flex flex-col gap-y-6 px-8">
          <For each={quizzes()} fallback={<>Loading...</>}>
            {(quiz, quizIdx) => {
              const handleQuizClick = () => {
                if (quizIdx() === 1) {
                  navigateToQuiz(quiz?.quizName || "");
                } else {
                  setSelectedQuizIdx((prevIdx) => prevIdx + quizIdx() - 1);
                }
              };

              let badge: { color: BadgeColor; text: string } | undefined =
                undefined;
              const score = quiz ? getPlayHistory(quiz.quizName) : undefined;

              if (score) {
                const scoreGrade = evalScore(score);

                if (scoreGrade === 1) {
                  badge = {
                    color: "blue",
                    text: "1",
                  };
                } else if (scoreGrade === 2) {
                  badge = {
                    color: "green",
                    text: "2",
                  };
                } else {
                  badge = {
                    color: "red",
                    text: "3",
                  };
                }
              }

              return (
                <div class="relative mx-auto">
                  {badge && (
                    <div class="absolute right-full bottom-full translate-y-2/3">
                      <Badge size="sm" color={badge.color}>
                        {badge.text}
                      </Badge>
                    </div>
                  )}
                  <div
                    onClick={quiz === null ? undefined : handleQuizClick}
                    class="hover:cursor-pointer relative"
                    classList={{ "text-3xl animate-bounce": quizIdx() === 1 }}
                  >
                    {quiz?.quizName}
                  </div>
                </div>
              );
            }}
          </For>
          {nextArrowVisible() && (
            <div onClick={() => setSelectedQuizIdx(1)}>⬇️</div>
          )}
        </div>
        <div class="fixed left-4 bottom-4">
          <Button color="lime" onClick={nextFont}>
            Font
          </Button>
        </div>
      </div>
    </BaseTemplate>
  );
};

export default Intro;
```

If you have experience with **React Router**, you will likely have no trouble using **SolidJS Router**.

In the quiz page, when a user selects a specific item, they will be navigated to the quiz page, and the selected quiz name will be delivered through the location state.

There will be three quiz names displayed on the screen, depending on which quiz is selected. Each quiz name will have a small badgeindicating the latest score, which will range from 1 to 3.

Furthermore, PC users will be able to use arrow keys to select the desired quiz.

As mentioned before, the signal defined in `fontSignal` can be used at any scope.

In React, you may have frequently used `map` to render array elements. In SolidJS, you can use the `For` or `Index` component to render array elements.

```
...
classList={{ "text-3xl animate-bounce": quizIdx() === 1 }}
...
```

If you tried the `classNames` package in React, you may find a similar concept. You can use conditional classes using `classList` attribute.

5\. Create **src/components/functions/KeyEvent/index.tsx**

```typescript
import { onCleanup, Component, onMount } from "solid-js";

export type Key = "Enter" | "ArrowUp" | "ArrowDown" | "1" | "2" | "3" | "4";

const KeyEvent: Component<{
  keys: Key | Key[];
  onKeyUp?: (key: Key) => void;
}> = (props) => {
  onMount(() => {
    const handleWindowKeyEvent = (e: KeyboardEvent) => {
      const k = e.key as Key;

      if (props.keys.includes(k)) {
        props.onKeyUp?.(k);
      }
    };

    window.addEventListener("keyup", handleWindowKeyEvent);

    onCleanup(() => {
      window.removeEventListener("keyup", handleWindowKeyEvent);
    });
  });

  return null;
};

export default KeyEvent;
```

I have defined `Key` type only for keys I expected users will use.

5\. Create **src/components/Button/index.tsx**

```typescript
import { Component, JSX } from "solid-js";

interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
  color: "lime";
}

const Button: Component<ButtonProps> = (props) => {
  const newClass = () => {
    let className = "rounded-lg p-1 cursor-pointer";

    switch (props.color) {
      case "lime":
        className += " text-white bg-lime-500";
    }

    if (props.class) className += ` ${props.class}`;

    return className;
  };

  return <button {...props} class={newClass()} />;
};

export default Button;
```

`newClass` will be called whenever there are changes to the `props.color` or `props.class` props.

6\. Create **src/components/Badge/index.tsx**

```typescript
import { Component, JSXElement, mergeProps } from "solid-js";

export type BadgeColor = "red" | "blue" | "green";

export type BadgeFontSize = "sm" | "md";

const Badge: Component<{
  color: BadgeColor;
  size?: BadgeFontSize;
  children?: JSXElement;
}> = (props) => {
  const mergedProps = mergeProps(
    {
      size: "sm" as BadgeFontSize,
    },
    props
  );

  const colorClasses = {
    red: "bg-red-500",
    blue: "bg-blue-500",
    green: "bg-green-500",
  };

  const sizeClasses = {
    sm: "px-2 py-1 text-sm",
    md: "px-3 py-2 text-base",
  };

  const badgeClasses = `${colorClasses[mergedProps.color]} ${
    sizeClasses[mergedProps.size]
  } text-white font-medium rounded-full inline-block w-fit`;

  return <span class={badgeClasses}>{props.children}</span>;
};

export default Badge;
```

In SolidJS, it's important to avoid destructuring props as it can cause them to lose their reactivity. When props are destructured, the individual properties lose their reactive nature.

To provide Default values for props, you can utilize the `mergeProps` function. With `mergeProps`, you can combine multiple props objects and maintain their reactivity.

7\. Create **src/lib/playHistory.ts**

```typescript
import { PlayHistory } from "../types/quiz";

const STORAGE_PLAY_HISTORY = "STORAGE_PLAY_HISTORY";

export const getPlayHistory = (title: string): number => {
  const historyMap = JSON.parse(
    localStorage.getItem(STORAGE_PLAY_HISTORY) || "{}"
  ) as PlayHistory;

  let score = 0;
  try {
    score = historyMap[title] || 0;
  } catch (e) {
    console.error(e);
    localStorage.removeITem(STORAGE_PLAY_HISTORY);
  }

  return score;
};

export const savePlayHistory = (title: string, score: number) => {
  const historyMap = JSON.parse(
    localStorage.getItem(STORAGE_PLAY_HISTORY) || "{}"
  ) as PlayHistory;

  try {
    historyMap[title] = score;
    localStorage.setItem(STORAGE_PLAY_HISTORY, JSON.stringify(historyMap));
  } catch (e) {
    console.error(e);
    localStorage.setItem(
      STORAGE_PLAY_HISTORY,
      JSON.stringify({
        [title]: score,
      })
    );
  }
};

export const evalScore = (score: number): 1 | 2 | 3 => {
  if (score >= 1) return 1;
  else if (score >= 0.6) return 2;
  else return 3;
};
```

The `savePlayHistory` function is responsible for storing the score in the local storage. If it fails to store the score for any reason, it recreates the score map to ensure data consistency.

The `getplayHistory` function retrieves the score from the local storage based on the quiz name. If there is no existing history for the quiz, it returns a score of 0.

The `evalScore` function calculates a grade between 1 and 3 based on the provided score, which ranges from 0 to 1. A score of 1 represents the best grade.

---

### Quiz Page

1\. Create **src/pages/Quiz/index.tsx**

```typescript
import { useLocation, useNavigate } from "@solidjs/router";
import Question from "../../components/Question";
import quizzesData from "../../data/quizzes";
import TopProgressBar from "../../components/TopProgressBar";
import BaseTemplate from "../../components/templates/BaseTemplate";
import {
  Show,
  createEffect,
  createMemo,
  createSignal,
  onMount,
  untrack,
} from "solid-js";
import { shuffleArray } from "../../lib/utils";
import { Quiz as TQuiz } from "../../types/quiz";

const Quiz = () => {
  const navigate = useNavigate();
  const location = useLocation<{
    quizName?: string;
  }>();
  const [quiz, setQuiz] = createSignal<TQuiz | undefined>();
  const [choices, setChoices] = createSignal<string[]>([]);
  const [quizListIdx, setQuizListIdx] = createSignal(0);
  const [correctQuestions, setCorrectQuestions] = createSignal<
    TQuiz["quizList"]
  >([]);
  const round = () => quizListIdx() + 1;
  const currentQuestion = createMemo(() => quiz()?.quizList[quizListIdx()]);

  createEffect(() => {
    const CHOICE_CNT = 4;
    const q = quiz();
    const currentQuizListIdx = quizListIdx();

    if (!q || q.quizList.length < CHOICE_CNT) return [];

    const choices: string[] = [q.quizList[currentQuizListIdx].answer];

    while (choices.length < CHOICE_CNT) {
      const randIdx = Math.floor(Math.random() * q.quizList.length);

      const alreadyExistInArray = choices.find(
        (c) => c === q.quizList[randIdx].answer
      );
      if (alreadyExistInArray) continue;

      choices.push(q.quizList[randIdx].answer);
    }

    setChoices(shuffleArray(choices));
  });

  const handleAnswer = (index: number) => {
    const q = quiz();
    if (!q) {
      alert("Something went wrong. Please, refresh the website.");
      return;
    }

    const userAnswer = choices()[index];
    const listIdx = quizListIdx();
    const currentQuestion = q.quizList[listIdx];

    if (currentQuestion.answer === userAnswer) {
      setCorrectQuestions((prevCorrectQuestions) =>
        prevCorrectQuestions.concat(currentQuestion)
      );
    }

    if (listIdx === q.quizList.length - 1) {
      navigate("/result", {
        state: {
          quiz: q,
          correctQuestions: correctQuestions(),
        },
      });
      return;
    }

    setQuizListIdx((prevIdx) => prevIdx + 1);
  };

  onMount(() => {
    const quiz = quizzesData.find(
      (q) => q.quizName === location.state?.quizName
    );

    if (!quiz) {
      navigate("/");
      return;
    }

    quiz.quizList = shuffleArray(quiz.quizList);
    setQuiz(quiz);
  });

  return (
    <BaseTemplate>
      <Show when={quiz() !== undefined} fallback={<div>Loading...</div>}>
        <TopProgressBar value={round()} max={quiz()?.quizList.length || 0} />
        <Question
          question={currentQuestion()?.question || ""}
          choices={choices()}
          onAnswer={handleAnswer}
        />
      </Show>
    </BaseTemplate>
  );
};

export default Quiz;
```

In the `onMount` function, it fetches a quiz name that is passed from the Intro page. It then shuffles the quiz list and updates the `quiz` signal with the shuffled list. If the quiz name does not exist in the data, it redirects the user to the home page.

Whenever the `quizIdx` changes, indicating the current question index, the choices are updated. One choice comes from the current question, and the other choices from the other questions.

Within the `createEffect` logic, there is an assurance that each choice is unique, preventing duplicates from being displayed.

The `Show` component is used for conditional rendering. In React, you would typically use the ternary operator or custom component. However, in SolidJS, the `Show` component is provided out of the library, making it easier to conditionally render components based on a given condition.

2\. Create **src/lib/utils.ts**

```typescript
export const shuffleArray = <T extends unknown>(list: T[]): T[] => {
  const newList = [...list];

  for (let i = 0; i < newList.length; i++) {
    const randIdx = Math.floor(Math.random() * list.length);

    const tmp = newList[i];
    newList[i] = newList[randIdx];
    newList[randIdx] = tmp;
  }

  return newList;
};
```

3\. Create **src/components/TopProgressBar/index.tsx**

```typescript
import { Component } from "solid-js";

const TopProgressBar: Component<{
  value: number;
  max: number;
}> = (props) => {
  const progressBarWidth = () =>
    Math.floor((props.value / props.max) * 100) + "%";

  const label = () => `${props.value} / ${props.max}`;

  return (
    <div class="fixed bg-sky-50 md:h-14 h-6 text-center top-0 left-0 right-0">
      <div
        class="absolute left-0 top-0 bottom-0 bg-sky-500 z-0"
        style={{
          width: progressBarWidth(),
        }}
      ></div>
      <div class="absolute left-0 top-0 bottom-0 right-0 flex justify-center items-center z-10">
        <span class="z-10 md:text-xl">{label()}</span>
      </div>
    </div>
  );
};

export default TopProgressBar;
```

It receives two props, `value` and `max`, and it displays the user's progress.

4\. Create **src/components/Question/index.tsx**

```typescript
import { Component, For } from "solid-js";
import KeyEvent, { Key } from "../../components/functions/KeyEvent";

const Question: Component<{
  question: string;
  choices: string[];
  onAnswer: (index: number) => void;
}> = (props) => {
  const handleKeyEvent = (key: Key) => {
    const index = Number(key) - 1;
    if (index >= props.choices.length) return;
    props.onAnswer(index);
  };

  const keysToDetect: Key[] = ["1", "2", "3", "4"];

  return (
    <>
      <KeyEvent keys={keysToDetect} onKeyUp={handleKeyEvent} />
      <div style={{ "padding-top": "20%" }} class="h-full">
        <div class="flex justify-center h-2/5">
          <p
            class="text-center text-1xl"
            style={{
              "max-width": "80vw",
            }}
          >
            {props.question}
          </p>
        </div>
        <ul class="text-center text-lg flex flex-col items-center gap-y-6">
          <For each={props.choices}>
            {(choice, index) => (
              <li
                class="bg-white rounded drop-shadow-md cursor-pointer hover:bg-slate-50 transition"
                style={{
                  width: "300px",
                  "max-width": "80vw",
                }}
                onClick={[props.onAnswer, index()]}
              >
                {index() + 1}. {choice}
              </li>
            )}
          </For>
        </ul>
      </div>
    </>
  );
};

export default Question;
```

The component displays a question and a set of choices to the user. For PC users, they can choose an answer using the keyboard Keys 1 to 4.

Regarding the `onClick` event handler for the `li` element, the syntax may look a bit unusual:

```javascript
onClick={[props.onAnswer, index]}
```

In SolidJS, you can pass arguments to a function using an array format.

---

### Result Page

Finally, the last page!

1\. Create **src/pages/Result/index.tsx**

```typescript
import { A, useLocation, useNavigate } from "@solidjs/router";
import BaseTemplate from "../../components/templates/BaseTemplate";
import { Show, createEffect, createSignal, onMount } from "solid-js";
import Badge from "../../components/Badge";
import { evalScore, savePlayHistory } from "../../lib/playHistory";
import { Quiz } from "../../types/quiz";

const Result = () => {
  type LocationState = {
    quiz: Quiz;
    correctQuestions: Quiz["quizList"];
  };
  const navigate = useNavigate();
  const [locationData, setLocationData] = createSignal<LocationState>();
  const [score, setScore] = createSignal<number>();
  const location = useLocation<LocationState>();
  const badgeComp = () => {
    const s = score();
    if (s === undefined) return null;

    const grade = evalScore(s);
    if (grade === 1) {
      return <Badge color="blue">PERFECT!</Badge>;
    } else if (grade === 2) {
      return <Badge color="green">GOOD!</Badge>;
    } else {
      return <Badge color="red">PRACTICE!</Badge>;
    }
  };

  createEffect(() => {
    const { quiz } = location.state || {};
    const s = score();
    if (!quiz || s === undefined) {
      return;
    }

    savePlayHistory(quiz.quizName, s);
  });

  createEffect(() => {
    const { quiz, correctQuestions } = location.state || {};
    if (!quiz || !correctQuestions) {
      return null;
    }

    const score = correctQuestions.length / quiz.quizList.length;
    setScore(score);
  }, []);

  onMount(() => {
    const { quiz, correctQuestions } = location.state || {};
    if (!quiz || !correctQuestions) {
      navigate("/");
      return;
    }

    setLocationData({
      quiz,
      correctQuestions,
    });
  });

  return (
    <BaseTemplate>
      <Show
        when={locationData() !== undefined}
        fallback={<div>Loading...</div>}
      >
        <div class="flex items-center justify-center p-4 h-full">
          <div class="flex flex-col items-center">
            {badgeComp()}
            <h1
              class="text-5xl h-1/3 mt-4"
              style={{ "overflow-wrap": "anywhere" }}
            >
              {locationData()?.quiz.quizName}
            </h1>
            <span class="text-4xl h-1/3 mt-4">{`${
              locationData()?.correctQuestions.length
            } / ${locationData()?.quiz.quizList.length}`}</span>
            <A href="/" class="text-1xl font-bold mt-4 animate-bounce">
              GO BACK HOME
            </A>
          </div>
        </div>
      </Show>
    </BaseTemplate>
  );
};

export default Result;
```

The component displays the number of questions answered correctly and the corresponding grade, which is 'Perfect', 'Good' or 'Practice'.

---

## Wrap Up

As first, I assumed that there wouldn't be many differences between `SolidJS` and `React`, but it was completely wrong.

In `SolidJS`, components are only called once, which took some time for me to adjust to since I was accustomed to `React`. but, this behavior feels natural.

If you're already familiar with `React`, translating to `SolidJS` might be relatively easier. You can gradually learn about the framework's concepts and features.

`SolidJS` provides components that I would typically use javascript built-in functions or use from other libraries, such as `For`, `Show`, `Index`, and more, right out of the box.

Overall, I thoroughly enjoyed my learning experience with `SolidJS`. It was a bit short time though. If you're considering trying out a new frontend framework, I highly recommend giving it a try.

By the way, if you come across any errors in this post, please feel free to leave a comment below. If you have any ideas for improving the app, let me know or submit pull requests in the Github repository!

Happy Coding!