App.jsx for Survey Application

App.jsx for Results Application

The survey taking application

The next application we are going to construct is the application that will allow users to take the surveys we constructed and uploaded to the server.

This application will display a list of questions and then offer a button at the bottom for the user to submit their responses.

State variables and downloading the survey

The only two state variables we will need for this application are variables to store the basic information about the survey (its title and its prompt) and the list of questions.

const [questions,setQuestions] = useState([]);
const [survey,setSurvey] = useState();

These things will need to be downloaded from the server when the application starts. One small problem that we will need to solve is how to specify which survey the user wants to take. I will solve this problem by hiding the survey number in the URL we send to the application. For example, to display the sample survey shown above I used a URL

http://localhost:5173/index.html#22

An application can access this URL information via a property of the global window object:

window.location.href

By using the JavaScript split() method we can break this URL string into to two parts on the # character and thus access the id number for the survey we want.

We now know that we will need to run a couple of fetch() requests when the application starts up to fetch the data for our state variables. The standard way to do something like this at application startup time is to use the React useEffect() hook. Normally, this hook is used to specify code to run when some state variable changes its value. A special application of useEffect() is to instead run some code when a component gets rendered for the first time.

Here now is the code that our application will use to fetch the data it needs:

useEffect(() => {getSurvey()},[]);

function getSurvey() {
  const url = window.location.href;
  if(url.indexOf('#') > 0) {
    let parts = url.split('#');
    let id = parts[1];
    fetch("http://localhost:8085/surveys?id="+id).then(response=>response.json()).then(response=>{setSurvey(response);getQuestions(id);});
  }
}
function getQuestions(id) {
  fetch("http://localhost:8085/questions?survey="+id).then(response=>response.json()).then(response=>setQuestions(response));
}

Displaying the survey

Here now is the rendering code for the App component:

if(survey)
  return (
    <div>
      <h3>{survey.title}</h3>
      <p>{survey.prompt}</p>
      {questions.map((q,i) => {return(<Question key={i} question={q} />); })}
      <button onClick={uploadAnswers}>Submit Answers</button>
    </div>
  )
else
    return (
      <div>
        <h3>No survey</h3>
        <p>No survey to display</p>
      </div>
    )

To display the individual questions I am using a Question component, which is similar to the QuestionPreview component I used for the survey builder application:

function Question({question}) {
  let handleChange = (e) => {question.response = e.target.value;};

  if(question.responses=='') {
    return (
      <div>
        <p>{question.question}</p>
        <input type="text" onChange={handleChange} />
      </div>
    )
  } else {
    const options = question.responses.split(',');
    return (
      <div>
        <p>{question.question}</p>
        <RadioGroup inline defaultValue={options[0]} onChange={handleChange}>
        {options.map((o) => {return(<Radio key={o} value={o}>{o}</Radio>)})}
        </RadioGroup>
      </div>
    )
  }
}

An important added task for this component is recording the user's responses to the questions. The strategy I used to handle this makes use of two important ideas. The first is that the prop I am passing to this Question component is a reference to a question object stored as one of the entries in the questions state variable. Since we are getting a reference to that question object we can modify that object if we need to. The second important idea here is to make use of onChange handlers for the input elements displayed in the component. The onChange handler I used here is a function that reads the current state of the input component and saves that state information in a response property in the question object we are connected to. This ensures that when the user clicks a radio button to indicate a choice or when they type text into a text input their inputs are going to be immediately saved in the question object. That means that when it comes time to upload the survey the question objects stored in the questions state variable will already contain the user's answers to all of the questions.

Uploading the responses

The code that renders the App component for this example also renders a button at the bottom of the survey that the user can click to submit their responses. The action function for that button is the following uploadAnswers() function:

function uploadAnswers() {
    questions.map(q => uploadResponse(q));
    alert("Your answers have been recorded.")
}
function uploadResponse(q) {
  let response = {survey:q.survey,question:q.idquestion,response:q.response};
  fetch('http://localhost:8085/responses', {
    method: 'POST',
    body: JSON.stringify(response),
    headers: {
      'Content-type': 'application/json; charset=UTF-8'
    }
  });
}

Since the server requires us to upload the response to each question separately, we map the uploadResponse() function across the list of question objects to upload the responses one by one.

Viewing the responses

The final part of the system is an application that displays summary data on the responses to a survey.

Most of the heavy lifting for this application is actually done by the back end server. By sending a request to the URL

http://localhost:8085/responses?survey=<id>

we can get the server to send us an array of objects that contains summary information on all of the questions in a survey. All we have to do is to put that information in the page. This simply requires us to create a single Response component to display the response data for a given question:

function Response({response}) {
  if(response.responses.length > 0) {
    return (
      <>
        <p>{response.prompt}</p>
        <ul>
        {response.responses.map(s => { return (<li>{s}</li>) }) }
        </ul>
      </>
    )
  } else {
    return (
      <>
        <p>{response.prompt}</p>
        <ul>
        {response.tallys.map(t => { return (<li>{t.choice}:{t.votes}</li>); }) }
        </ul>
      </>
    )
  }
}

Just as with the previous application, we will use the useEffect() hook to send the request for data to the server at startup time. Then all the App object has to do is to wait for the data to arrive and then render a list of Response elements when it does arrive:

function App() {
  useEffect(() => {getSurvey()},[]);

  const [responses,setResponses] = useState([]);
  
  function getSurvey() {
    const url = window.location.href;
    if(url.indexOf('#') > 0) {
      let parts = url.split('#');
      let id = parts[1];
      fetch("http://localhost:8085/responses?survey="+id).then(response=>response.json()).then(response=>{setResponses(response);});
    }
  }
  
  if(responses.length > 0)
    return (
      <div>
        <h3>Survey Responses</h3>
        {responses.map(r => {return(<Response response={r} />); })}
     </div>
    )
  else
      return (
        <div>
          <h3>No data</h3>
          <p>No data to display</p>
        </div>
      )
}