<info 340/>

AJAX

Tim Carlson
Winter 2024

View of the Day

  • Q&A (poll everywhere)

  • AJAX ("lecture")

  • fetch() and Promises (code demo)

  • Effect Hooks (code demo)

Questions?

HTTP Requests

protocol

domain

resource

"Hi Wikipedia, I'd like you to send me the Informatics page!"

two

t
 

(how to handle info)

(who has info)

(what info you want)

Browsers submit HTTP requests to a server when you follow a hyperlink or submit a form.

Form Attributes

We use attributes on a <form> to specify the HTTP Request

<form role="form" method="GET" action="/signup">

  <label for="unameBox">Name:</label>
  <input type="text" name="username" id="unameBox">

  <button type="submit">Sign up!</button>

</form>

GET: (default) sends a GET request; data is appended to the URI as a query parameter (e.g., ?username=value)

POST: sends a POST request; data is included in the request body.

The URI

HTTP Verbs

HTTP requests include a target resource and a verb (method) specifying what to do with it

GET Return a representation of the current state of the resource
POST Add a new subresource (e.g., insert a record)
PUT Update the resource to have a new state
PATCH Update a portion of the resource's state
DELETE Remove the resource
OPTIONS Return the set of methods that can be performed on the resource

Intercepting Events

Here's the code to intercept the event with our demo form

Function App(props) {
  const [stateData, setStateData] = useState(EXAMPLE_DATA);
  //control form
  const [queryInput, setQueryInput] = useState('');

  const handleChange = (event) => {
    setQueryInput(event.target.value);
  }
   const handleSubmit = async (event) => {
    event.preventDefault();
  }
...
  <form method="GET" action="https://api.github.com/search/repositories">
    <input type="text" className="form-control mb-2" 
     name="q"
     placeholder="Search Github for..."
     value={queryInput} onChange={handleChange}
     />
    <input type="hidden" name="sort" value="stars" />
    <button type="submit" className="btn btn-primary">Search!</button>
  </form>

Next we need to make the http request ourselves with script

Our Form

     return (
    <div className="container">
      <header><h1>AJAX Demo</h1></header> 

      <form method="GET" action="https://api.github.com/search/repositories">
        <input type="text" className="form-control mb-2" 
          name="q"
          placeholder="Search Github for..."
          value={queryInput} onChange={handleChange}
        />
        <input type="hidden" name="sort" value="stars" />
        <button type="submit" className="btn btn-primary">Search!</button>
      </form>

      <div className="mt-4">
        <h2>Results</h2>
        {/* results go here */}
        {dataElemArray}
      </div>
    </div>
  )

Get

Action

Inputs are query params

A technique for having code (JavaScript) send an HTTP Request, rather than the browser

XML

EXtensible Markup Language

A generalized syntax for semantically defining structured content (HTML with own tags!)

<person>
   <firstName>Alice</firstName>
   <lastName>Smith</lastName>
   <favorites>
      <music>jazz</music>
      <food>pizza</food>
   </favorites>
</person>

XML

JSON

<breakfast_menu>
  <food>
    <name>Belgian Waffles</name>
    <price>$5.95</price>
    <description>
      Two of our famous Belgian Waffles with plenty of real maple syrup
    </description>
    <calories>650</calories>
  </food>
  <food>
    <name>Strawberry Belgian Waffles</name>
    <price>$7.95</price>
    <description>
      Light Belgian waffles covered with strawberries and whipped cream
    </description>
    <calories>900</calories>
  </food>
  <food>
    <name>Berry-Berry Belgian Waffles</name>
    <price>$8.95</price>
    <description>
      Light Belgian waffles covered with an assortment of fresh berries and whipped cream
    </description>
    <calories>900</calories>
  </food>
  <food>
    <name>French Toast</name>
    <price>$4.50</price>
    <description>
      Thick slices made from our homemade sourdough bread
    </description>
    <calories>600</calories>
  </food>
  <food>
    <name>Homestyle Breakfast</name>
    <price>$6.95</price>
    <description>
      Two eggs, bacon or sausage, toast, and our ever-popular hash browns
    </description>
    <calories>950</calories>
  </food>
</breakfast_menu>
{
  "breakfast_menu": {
    "food": [
    {
      "name": "Belgian Waffles",
      "price": "$5.95",
      "description": "Two of our famous Belgian Waffles with plenty of real maple syrup",
      "calories": "650"
    },
    {
      "name": "Strawberry Belgian Waffles",
      "price": "$7.95",
      "description": "Light Belgian waffles covered with strawberries and whipped cream",
      "calories": "900"
    },
    {
      "name": "Berry-Berry Belgian Waffles",
      "price": "$8.95",
      "description": "Light Belgian waffles covered with an assortment of fresh berries and whipped cream",
      "calories": "900"
    },
    {
      "name": "French Toast",
      "price": "$4.50",
      "description": "Thick slices made from our homemade sourdough bread",
      "calories": "600"
    },
    {
      "name": "Homestyle Breakfast",
      "price": "$6.95",
      "description": "Two eggs, bacon or sausage, toast, and our ever-popular hash browns",
      "calories": "950"
    }
    ]
  }
}

A technique for having code (JavaScript) send an HTTP Request, rather than the user

JSON

XMLHttpRequest

AJAX requests are built on a browser-provided object called XMLHttpRequest. We don't use this method because it is overly verbose and complicated.

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
       // Action to be performed when the document is read;
       var xml = xhttp.responseXML;

       var movie = xml.getElementsByTagName("track");
       //...
    }
};
xhttp.open("GET", "filename", true);
xhttp.send();

fetch()

The modern method for submitting XmlHttpRequests. Included in the DOM's API.

//send an AJAX request to the given url
fetch('url');

Not supported by all browsers! http://caniuse.com/#search=fetch Can add API features to browsers that do not yet support them by including a polyfill: an external library (code) that replicates that API.

https://github.com/github/fetch, or install whatwg-fetch library

//in index.js
import 'whatwg-fetch'
//Slide 15

import React from 'react';
import ReactDOM from 'react-dom/client';

import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App.js';

import 'whatwg-fetch' //load the polyfill we just installed via npm

const URL = "https://api.github.com/search/repositories?q=react&sort=stars";
const result = fetch (URL);
console.log(result);


//render the root
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Install and Import whatwg-fetch

import

Asynchronous

AJAX requests are asynchronous, so happen simultaneously with the rest of the code.

That means that after the request is sent, the next line of code is executed without waiting for the request to finish!

   console.log('About to send request');

   //send request for data to the url
   fetch(url);

   console.log('Sent request');

(1)

(2)

(3)

(4) Data is actually received some time later,
      and Promise is fulfilled

does NOT return the data,

but a Promise for it

Promises

Promises

We use the .then() method to specify a callback function to be executed when the promise is fulfilled (when the asynchronous request is finished)

//what to do when we get the response
function successCallback(response) {
   console.log(response);
}

//when fulfilled, execute the callback function
//(which will be passed the http response)
const promise = fetch(url);
promise.then(successCallback);


//more common to use anonymous variables/callbacks:
fetch(url).then(function(response) {
   console.log(response);
});

reads like English?

callback will be passed the request response

//Slide 18

import React from 'react';
import ReactDOM from 'react-dom/client';

import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App.js';

import 'whatwg-fetch' //load the polyfill we just installed via npm

const URL = "https://api.github.com/search/repositories?q=react&sort=stars";

console.log("about to send");
const aPromise = fetch (URL);

aPromise.then(function(response) {
    console.log(response);
  	console.log("response received");
})

console.log("request sent");

//render the root
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Basic fetch in index.js

Url to hit

fetch

Promise is returned

But we don't have the json object yet

fetch() Responses

The parameter fetch() passes to its .then() callback is the http response, not the data itself!

The response to an HTTP request (such as from fetch()) has two parts:

  1. Header with information about the response. Like a postal envelope.

  2. Body with the content (data) of the response. Like a postal letter.

HTTP Response Codes

api

Encoding the Body

The parameter passed to the .then() callback is the response, not the data we're looking for.

So we need to extract the data from that response.

The fetch() API provides a method .json() that we can use to encode the data from the response into a readable format... but this method is also asynchronous and returns a promise!

fetch(url).then(function(response) {
   const newPromise = response.json();


   //... what now?
});

not the data

another promise

//Slide 23
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './components/App.js';
import 'whatwg-fetch' //load the polyfill we just installed via npm

const URL = "https://api.github.com/search/repositories?q=react&sort=stars";

const aPromise = fetch (URL);

const response = aPromise.then(function(response) {
    console.log(response);
    console.log("response received");
    
    const encodePromise = response.json();
    console.log(encodePromise);

    encodePromise.then(function(data) {
      console.log(data)
    })
   
})

2 promises, 2 .then()s

fetch

.then on the 1st promise

.then on 2nd promise

now we have the data!

we have the response, still need to unpack json object

Returning Promises

If the .then() callback itself returns a Promise, then the "original" promise will take on the status and data of that returned promise (e.g., be replaced with the new IOU)

const originalPromise = getAsyncData(myFirstSource).then(function(firstData){
    //do something with `firstData`

    const newPromise = getAsyncData(mySecondSource); //a second async call!
    return newPromise; //return the promise.
}); //`originalPromise` now takes on the status and data of `newPromise`

originalPromise.then(function(secondData){
    //do something with `secondData`, the data downloaded from `mySecondSource`
});

All together

fetch(url)
    .then(function(response) {
        const dataPromise = response.json();
        return dataPromise;
    })
    .then(function(data) {
        //do something with the data!!
        console.log(data);
    });

Together in the cleaner format (index.js)

//Slide 26
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App.js';

import 'whatwg-fetch' //load the polyfill we just installed via npm

const URL = "https://api.github.com/search/repositories?q=react&sort=stars";


fetch (URL)
    .then(function(response) {
       const dataPromise = response.json() 
       return dataPromise;
    })
    .then (function(data) {
        console.log("data", data);
    })


//render the root
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

fetch block

We only have access here. We likely set a state variable here

Moved to within App Component

function App(props) {
...
  const handleSubmit = async (event) => {
    event.preventDefault();
     const URL = "https://api.github.com/search/repositories?q="+queryInput+"react&sort=stars";

    fetch(URL)
      .then(function (response) {
        const dataPromise = response.json()
        return dataPromise;
      })
      .then(function (data) {
        console.log("data", data);
      })

  }

  const dataElemArray = stateData.map((repo) => {
    return <li key={repo.html_url}><a href={repo.html_url}>{repo.full_name}</a></li>
  })

  return (
...
    {dataElemArray}
...
  )
}

In a Component

Fetch Block

If we were going to use, we would setState here

added query params

Use State Variable to hold

function App(props) {
  const [stateData, setStateData] = useState([]);
  const [queryInput, setQueryInput] = useState('');

  const handleChange = (event) => {
    setQueryInput(event.target.value);
  }

  const handleSubmit = async (event) => {
    event.preventDefault();
    const URL = "https://api.github.com/search/repositories?q="+queryInput+"react&sort=stars";

    fetch(URL)
      .then(function (response) {
        const dataPromise = response.json()
        return dataPromise;
      })
      .then(function (data) {
        console.log("data", data);
        setStateData(data.items);
      })
  }
  
  ...

setState to set the data, and to cause re-render

set the variable state to an empty array

Grabbing a json object from public folder

function App(props) {
  const [stateData, setStateData] = useState([]);
  ...
  useEffect(() => {
    fetch('data.json')
    .then(function (response) {
      const dataPromise = response.json()
      return dataPromise;
    })
    .then(function (data) {
      setStateData(data);
    })
    }, [])
 ...
  const handleSubmit = async (event) => {
    event.preventDefault();
    const URL = "https://api.github.com/search/repositories?q="+queryInput+"react&sort=stars";
    fetch(URL)
      .then(function (response) {
        const dataPromise = response.json()
        return dataPromise;
      })
      .then(function (data) {
        setStateData(data.items);
      })
  }
...

this is the interaction based fetch (in handleSubmit)

Effect Hook so only runs once

what other vars cause it to run a 2nd time

Load data when component first runs

Catching Errors

We can use the .catch() function to specify a callback that will occur if a promise is rejected (an error occurs). This method will "catch" errors from all previous .thens

fetch(url)
  .then(function(data) {
     return response.json();
  })
  .then(secondCallback)
  .catch(function(error) {
     //called if EITHER previous callback
     //has an error

     //param is object representing the error itself
     console.log(error.message);
  })
  .then(thirdCallback) //"finally"

async/await

Managing Promise callback chains can get tricky. ES 2017 introduced a new set of keywords async and await that can let you write Promise-based code synchronously.

//an `async` function is one that runs asynchronously
//(meaning it returns a promise)
async function myAsyncFunction() {
  
  //`await` indicates that the code should "hold" until
  //the asynchronous promise is fulfilled
  //can only be used inside of an `async` function
  const response = await fetch(url); //wait for fetch
  const data = await response.json(); //wait for encode
  
  console.log(data); //can use data ("synchronously")  

}

fetch using async/await

//Slide 32

import React from 'react';
import ReactDOM from 'react-dom/client';

import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App.js';

import 'whatwg-fetch' //load the polyfill we just installed via npm

const URL = "https://api.github.com/search/repositories?q=react&sort=stars";

async function myAsyncFunction() {
  
  const response = await fetch(URL); //wait for fetch
  const data = await response.json(); //wait for encode
  console.log(data); //can use data ("synchronously")  

}

myAsyncFunction();

//render the root
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

'async' keyword required

'await' can now be used

fetch() in React

You can use fetch() in response to user interactions (e.g., button clicks) by calling the function and then assigning any data to a state variable.

function MyComponent(props){
  //initialize state as empty
  //make sure component doesn't error with this initial value!
  const [stateData, setStateData] = useState([])
  
  const handleClick = (event) => {
    fetch(dataUri) //send AJAX request
      .then((res) => res.json())
      .then((data) => {
        //do any data processing here...
        setStateData(data); //assign data to state
                            //rerenders using data
      })
  }
  
  return ...  
}

Effect Hooks

To fetch() data when the component first renders, you need to use an effect hook (which runs after the first render)

//import the hooks used
import React, { useState, useEffect } from 'react';

function MyComponent(props) {
  const [stateData, setStateData] = useState([]);

  //specify the effect hook function
  useEffect(() => {
    fetch(dataUri) //send AJAX request
      .then((res) => res.json())
      .then((data) => {
        //do any data processing here...
        setStateData(data); //assign data to state
      })
  }, []) //array is the second arg to the `useEffect()` function
         //It lists which variables will "rerun" the hook if they 
         //change

  return (<div>...</div>)
}

Effect Hook Cleanup

In order to "clean up" any work done in an effect hook (e.g., disconnect listeners), have the callback return a "cleanup function" to execute. Not needed for fetch()

import React, { useState, useEffect } from 'react';

function MyComponent(props) {
  //specify the effect hook function
  useEffect(() => {

    //...do persistent work, set up subscriptions, etc

    //function to run when the Component is being removed
    function cleanup() {
      console.log("component being removed")
    }    
    return cleanup; //return function for React to call later    
  }, [])

  return (<div>...</div>)
}

Action Items!

  • Review Ch 18, 16.3 (AJAX)

  • Read Ch 19: Firebase

  • Problem Set 08 due Monday (it's small)

    • It's small! Get it done!

  • Problem Set 09 due next Friday (it's small)

    • It's small! Get it done!

Next time: Firebase databases!

info340wi24-ajax

By Tim Carlson

info340wi24-ajax

  • 65