Federico Gambarino

RxJS for a reactive quiz

March 18, 2021
RxJS, Angular

Recently I started to properly study Japanese. I love Japan (I have been there two times already), and I am fascinated by the culture, language and, obviously 😜, cuisine.
So I decided to take some private (and remote) lessons, with the idea of achieving - some day in the future - the N5 or the N4 JLPT certification. I already knew Katakana and Hiragana alphabets, but only a few Kanji.
For my study I can use many available online platforms, but I wanted to create something by myself, to study them while having fun with a new side project. So I created Kanji Time!.

It’s a very simple Angular web app, where you can see all the Kanji for the N5 JLPT certification (but I will soon add the ones of N4 as well), and test yourself.
The feature I appreciated developing the most is the latter, where I had so much fun trying to use only RxJS to implement a Quiz.
Let’s see together how I made it.

Introduction

As I said before in Kanji Time! there are two features, explore and practice. The first one let you scroll and see Kanji and their readings, the other one lets you test your readings knowledge. For both I manually wrote a single JSON file, where the Kanji are defined, with some of their properties, for examples meaning, number of strokes and so on.

This is the page of the quiz, where I highlighted the two main components:

Quiz Component

QuizComponent is the container, it has the data, and some simple logic to provide to its presentational child component - the QuestionComponent.
The QuestionComponent is responsible for displaying a clear question and three buttons, one for each option.
When the user clicks on one of those buttons, the QuestionComponent sends to its parent, through an Output property, if the answer is correct or not.
Then a new Kanji is provided by the parent, waiting for a new answer, until all kanji have been tested. To me this is a perfect flow for an asynchronous stream, like the Observables are. Of course, we can obtain this behavior using just some arrays, but observables are much more powerful.

RxJS at work

Let’s start looking at some code, this is how I retrieve the questions:

const questions$: Observable<Question[]> = this.practiceService.getQuestions()

questions$ is the observable that comes from the http call. The response of the call gives us an array of Question.
We need to transform the array of N elements into one Observable that returns N values before completing.
For this we can use a feature that is available in different RxJS operators - like switchMap, mergeMap and others as well -, the ability to transform arrays, iterables, promises and other things into an observable.
It’s much simpler when you look at the code:

const flattenedQuestions$: Observable<Question> = this.questions$.pipe(
  switchMap(questions => questions)
)
// equivalent to something like [1,2,3] => (1,2,3, complete)

Since I want the questions randomly sorted I use a simple custom function to shuffle the array before it being flattened, like this:

const flattenedShuffledQuestions$: Observable<Question> = this.questions$.pipe(
  switchMap(questions => shuffleArray(questions))
)
// equivalent to something like [1,2,3] => (2,3,1, complete)

Now that we have an observable where every value emits is a question, I need a way to have a new question only when an answer is given.
For this, the zip operator is perfect. You can see graphically how it works in my old side project RxJS Intro in the observable operators page. Fundamentally, I can synchronize more observables with it, it combines different observables together, waiting for the nth emitted value of every observable, then emits these values combined in an array.
I can then use a Subject or a BehaviorSubject and let it emit a value just when an answer arrives (i.e. when one of the three options button is clicked).
I opted for the BehaviorSubject, since I want an answer available right at the start of the quiz, and the BehaviorSubject already has a starting value by definition.

export class QuizComponent implements OnDestroy {

  private showNextQuestion$: BehaviorSubject<boolean> = new BehaviorSubject(
    true
  );

  startTheQuiz() {

    const questions$ = this.practiceService.getQuestions();

    zip(
      this.showNextQuestion$,
      questions$.pipe(
        switchMap((questions) => {
          return shuffleArray(questions);
        })
      )
    )
      .pipe(takeUntil(
        //subject to properly unsubscribe at the destroy of the component
        this.destroy$))
      .subscribe({
        next: ([, kanjiQuestion]) => {
          if (kanjiQuestion) {
            // prepare the question for the child component
          }
        },
        complete: () => {
          // show the result of the quiz
        },
      });
  }

  onAnswer() {
    this.showNextQuestion$.next(true);
  }
}

This seems fine, but it’s not totally correct, since when the last question is emitted, the observable completes as well, leaving the last question unanswered. To fix this we can concatenate a new observable that emits only one value, right after the questions$ observable completes, using the concat operator. With this, we are able to answer to the last question, and only then see the result page.

startTheQuiz()
{
  const questions$ = this.practiceService.getQuestions();

  zip(
    this.showNextQuestion$,
    concat(
      questions$.pipe(
        switchMap((questions) => {
          return shuffleArray(questions);
        })
      ),
      of(null)
    )
  ) // and so on
}

perfect then, we now have a way to answer to one question at a time. To me, it’s not enough though. I want to know, at least, how well I’m progressing and how many questions I still have to answer. As you can see here:

Progress and status of the quiz

To have the total number of questions we can use the length of the questions array, but how exactly?

This is the quickest way:

  zip(
    this.showNextQuestion$,
    concat(
      questions$.pipe(
        switchMap((questions) => {
          // ❌ assigning the questions total number
          // in the body of the function
          this.totalQuestions = questions.length;
          return shuffleArray(questions);
        })
      ),
      of(null)
    )
  ) // and so on

this works, but any side effect, like the assignment of a variable, inside the body of a function used by an operator, is not a good practice. Similarly to the functional paradigm, the reactive one expects to avoid any side effects inside the operators chain.
It’s only a little better using the tap operator, since its main purpose is to be used for side effects but only when other ways are not available.
The best option is to define all sort of side effects inside the subscribe method. In this particular case, though, we lost the length of the array due to the flattening done by the switchMap, and, moreover, I don’t really want to do a contrived mapping to have, every time a new question is emitted, the total number of questions.
We can do something much simpler: we can subscribe again to the same observable, and we will derive the total number of questions right away.

startTheQuiz()
{
  const questions$ = this.practiceService.getQuestions();

  questions$.subscribe((questions) => {
    this.totalQuestions = questions.length;
  });
  
  zip(
    this.showNextQuestion$,
    concat(
      questions$.pipe(
        switchMap((questions) => {
          return shuffleArray(questions);
        })
      ),
      of(null)
    )
  ) // and so on
}

this works just fine, it’s easy to read, and the derived observables have a single responsibility. Still, something can be improved, can you see the problem here?

We subscribe to the questions$ observable two times, and since it’s the observable derived by a http call, it means we are fetching the questions two times.
How can we have only a single fetch? Using the share operator. The share operator let you transform a cold observable into a hot observable. It means that from a unicast you have a multicast: it shares the original observable between every subscriber.

startTheQuiz()
{
  // thanks to share the service will perform a http call just once
  const questions$ = this.practiceService.getQuestions().pipe(share());

  questions$.subscribe((questions) => {
    this.totalQuestions = questions.length;
  });

  zip(
    this.showNextQuestion$,
    concat(
      questions$.pipe(
        switchMap((questions) => {
          return shuffleArray(questions);
        })
      ),
      of(null)
    )
  ) // and so on
}

We now have the total number of questions, but I would like to know also: how many questions I have answered to and how well I’m performing (i.e. a percentage of the correct answers).
Of course, we could use a variable to store the data in, but since I’m using RxJS why limiting myself to this?
I just need a Subject to use when answering, to send the information if the answer is correct or not.

answersAndSuccess$: Subject<boolean> = new Subject<boolean>();

onAnswer(correct: boolean) {
  this.answersAndSuccess$.next(correct);
  this.showNextQuestion$.next(true);
}

then, with it, it’s quite straightforward to obtain what I want:

  answeredQuestions$ = this.answersAndSuccess$.pipe(
    scan((acc) => acc + 1, 0)
  );
  correctAnswers$ = this.answersAndSuccess$.pipe(
    scan((acc, value) => (value ? acc + 1 : acc), 0)
  );
  percentageCorrect$ = zip(
    this.correctAnswers$,
    this.answeredQuestions$
  ).pipe(map(([correct, answered]) => (correct / answered) * 100));

with the scan operator we can use an accumulator and perform operations for each value emitted in the observable. It’s very similar to the reduce operator, with the only difference that scan emits every single value computed, while reduce emits just when the original observable completes. For the answeredQuestions$ I am just adding 1 to the accumulator, that here starts from 0, every time a new answer arrives.
For the correctAnswers$ I need, instead, to add 1 to the accumulator only when the answer is correct.
With those I can combine them and obtain an observable that, every single time an answer is given, emits the current percentage of correct answers, this is the percentageCorrect$ observable.

Here is a piece of the final result:

<ng-container *ngIf="{
  answered: answeredQuestions$ | async,
  percentageCorrect: percentageCorrect$ | async
} as answers">
 <div class="progress">
   <div class="progress--number">
     {{ answers.answered + 1 }}/{{ totalQuestions }}
   </div>
   <div
     class="progress--percentage"
     [style.visibility]="answers.answered ? null : 'hidden'"
   >
     Correct: {{ answers.percentageCorrect | number: '1.0-0' }}%
   </div>
 </div>
 <ui-question
   [question]="kanjiQuestion"
   (answer)="onAnswer($event)"
 ></ui-question>
</ng-container>

Just a comment about this: as you can see I use the ngIf directive with an expression that defines an object. This is a trick that lets you define in a single point the different values that derives from each observable with the async pipe.
Since the object definition is always truthy, the template is always rendered.

Final thoughts

This solution could be achieved without the use of RxJS at all, but RxJS is very flexible, and if understood, less bug prone. For instance if you would like to add some delay between the answer and the appearing of the next question, you could simply use the delay operator.
Yes, an array based solution could use setTimeout instead, but the declarative approach of RxJS is far better.

I know that RxJS can be really tough, but once you learn it I assure you that you cannot work without it!

The code I show here is an extract of what is in the Kanji Time! repository.

...and that's it for today! Thank you for your time 😃

Federico


© 2022, Federico Gambarino - All Rights Reserved

Made with ❤️ with Gatsby