5.7. React: Sorting Question - Version 1

To follow along, open up the file questions/examples/SortingQuestion_v1/SortingQuestion_v1.jsx.

note:The .jsx extension is used to identify JavaScript files that use Babel and ES6 syntax, which is often called JSX.

To make use of React, we need to include the appropriate libraries - this is done with the require statement in ES6.

Gulp & Browserify

Internally, Quizzera makes use of a tool called Browserify which packages modules in a bundle. We also use Gulp, which is a task runner for JavaScript.

To learn more about these tools and how they’re often used, check out the Browserify and Gulp documentation.

The JSX file for a question must be named the same as the question module to be detected - otherwise, it will be ignored.

We require the installed React and react-dom libraries, along with a custom React class Question:

1
2
3
4
var React = require('react'),
    ReactDOM = require('react-dom'),
    ReactBootstrap = require('react-bootstrap'),
    Button = ReactBootstrap.Button;

Reusable React Modules

Browserify is picky in how to require modules. So, our Gulp task that manages compiling the JSX files constructs certain search paths given app-relative paths.

For example, the questions/Question.jsx path will resolve to ./questions/static/react/questions/Question.jsx, where the . is the main root directory of the project.

For any require of the form {app}/... where {app} is a Django application, the filepath is expanded to ./{app}/static/react/{app}/....

This path structuring borrows heavily from how Django recommends structuring templates/static files for an app.

5.7.1. Rendering

The react-dom library is used to render the React component onto an HTML DOM element. So, we call the ReactDOM.render in our main function; this will render our question:

1
2
3
4
5
6
7
var Question = require('questions/Question.jsx');

function main() {
    /* Render the primary component onto the DOM element. */
    ReactDOM.render(
        <SortingQuestion />,
        document.getElementById('question')

The code itself is very straightforward - it creates an instance of the component class with <SortingQuestion /> and then renders it into the DOM element with the ID of question.

But wait, does this element come from?

5.7.1.1. Question Templates

Each question class exports the name of the Django template to use for rendering the question using the question_template attribute or get_question_template method (or both).

The default location is templates/question.html:

1
2
3
4
5
6
7
8
{% extends 'questions/question.html' %}
{% load staticfiles %}

{% block javascript %}
    <script type='text/javascript'
        src = "{% static 'questions/js/bundle/SortingQuestion_v1.js' %}">
    </script>
{% endblock %}

This template is simple - it extends the questions/question.html template, which is the default starting template for any module. This base template should always be extended.

In addition, it loads the JS file questions/js/bundle/SortingQuestion_v2.js. This is the output location from Gulp - a question JSX file named Question.jsx will be compiled, bundled, and then placed to questions/js/bundle/Question.js.

It’s important that all JavaScript code is placed between the {% block javascript %} and {% endblock %} template tags - otherwise, that code won’t be rendered in the template and thus, will never get executed.

You may notice that here’s no element with an ID of question defined here, though.

This is defined in the template that we extended:

1
   <div id='question'></div>

5.7.2. The SortingQuestion Component

A React component is simply a JavaScript function. However, with ES6 syntax, a component can also be declared by using a class that extends React.Component - which is exactly what we do.

Constructors in ES6 syntax are conveniently named constructor(). For all React components, the constructor should take in a single argument: props. These are the properties passed in when creating the component - our question components don’t take or use any props, but internal components will often do so.

We first declare our class and constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    }

class SortingQuestion extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            attempt: { /* Incoming attempt data. */
                prompt: ''
                },
            submitted: new Object(), /* Data received from submitting attempt */
            finished: false, /* Is the question finished? */
            request: true, /* Is the attempt creation request finished? */
            submitRequest: true, /* Is the attempt submission request finished? */
note:

The initial state of the component is set in the constructor. The state fields of attempt, submitted, finished, request, and submitRequest have special meanings that we will go into later.

For now, know that those fields are set when data is received from the API.

We’ll skip the submit() method for now and get back to it later, once we understand when it will be called.

5.7.3. Rendering the Component

The component’s render() method, as the name implies, renders the component. This method will be called internally, by React, whenever the state changes.

The first few lines of this code are simple - it sets styling for various components if the appropriate data is not present:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  render() {
      /* Render the component. */
      var extraInputProps = {};

      /* If the question is already finished, then the user's previous submitted
      answer should be displayed instead of allowing new input. */
      if (this.state.finished)
          extraInputProps.value = this.state.submitted.answer;
      else extraInputProps.defaultValue = this.state.submitted.answer;

      /* If no explanation is present (i.e when rendering the question without
      submitting), hide the explanation display. */

The most interesting code starts with the return statement. Specifically, all of the entire rendered data is wrapped in a Question component.

The rendered output of any question’s component should be wrapped in a Question component. That wrapper component handles making all of the API requests, formatting and parsing the outgoing/incoming data, and ensuring that any errors are adequately reported.

There are two callbacks attached to the Question component, being the onData and ref callbacks. The ref callback is for React - it allows you to keep a reference to the component. The reference is required so that you can submit the user’s attempt when needed.

The onData callback, on the other hand, is called by the Question component when the API responds with data. If your state fields are named as recommended, then the callback can just be this.setState(data), which is what we have.

The Question Component

The component wraps around all questions and interacts directly with the Quizzera API. It abstracts away a lot of the tedious, repetitive code required for questions. In addition, it provides the standard interface that questions are contained within.

If you want to look at the code, check out the file questions/static/react/questions/Question.jsx.

5.7.3.1. Prompt

The onData callback is responsible for setting the fields of our component based on the data the Question component receives. The response from the API when generating a new question attempt, which is the stored in the attempt state field, is the value returned from your question module’s clean_instance(instance, False) method.

So, to display the prompt, we access that field:

1
2
3
4
     return (<Question onData={(data) => this.setState(data)}
         ref={(q) => this.question = q}>
         {/* Prompt display. */}
         <pre><code>

5.7.3.2. User Input

The user input, for our example purposes, is a text input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

         {/* Answer submission. */}
         <div id ="submission">
             <h3>Answer</h3>
             
             <input type='text' ref={(input) => this.input_elem = input}
                 {...extraInputProps} />
             <Button onClick={() => this.submit()} disabled={this.state.finished}
                 bsStyle='primary'>
                 Submit

There are a few things to note here. First, we keep a reference to the input component with a ref callback, as we did before - we need a reference to get the user’s input value.

Here, we use JSX’s spread syntax to pass in the extraInputProps. Essentially, those props contain the value of the user’s answer if the user is viewing an attempt they already submitted before.

Next, we have a submission button. When the button is clicked, we call our own submit() method. In addition, the input is disabled if the student has already finished the submission - that is, if they are viewing an attempt from before.

Let’s take a look at the submit() method:

1
2
3
4
5
       }

   submit() {
       /* Submit the answer. */
       var answer = this.input_elem.value;

The method is very simple - it takes the user answer from the input component’s reference and then submits that answer to the question. The Question.submitAnswer method takes care of sending the answer to the API, parsing the response, and passing us the resultant data with our provided onData callback.

note:this.question is the reference to the Question component created at the start of the rendering.

5.7.3.3. Submitted Data

The API’s response for submitting the answer, which will call your validate_answer and clean_instance(instance, True) methods, is set to the submitted field. However, this field contains extra data, introduced by the question manager. Particularly, the format of the data is:

{
  score: float,
  max_score: float,
  data: {...} // Your validate_answer's response
}

So, we display the user’s score out of the max score, along with the correct answer and explanation, which is provided in our instance data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
         </div>

         {/* Answer & score. display */}
         <div style={explanationStyle}>
             <h5>Score</h5>
             <h4>
                 {this.state.submitted.score} / {this.state.submitted.max_score }
             </h4>

             <h5>Correct Answer</h5>
             <pre><code>
                 {this.state.submitted.data? this.state.submitted.data.correct_answer:
                     null}
             </code></pre>

             <h5>Explanation</h5>
             <pre><code>

5.7.4. Viewing Previous Attempts

That’s all there is to a question component! We displayed the prompt, an input, and a button to submit that input. In addition, we display the submission result once the user has submitted their answer.

The Question component handles displaying previous attempts. In fact, it will intelligently provide the data, in the same format as the union of the attempt and submitted state fields, so that there are no new code changes required when viewing an older attempt.

Next, we’ll look at a slight improvement of the component using some common shared code to avoid duplication.