Practical: Working with an API

Initial Due Date: 2024-10-08 11:59PM Final Due Date: 2024-10-25 4:00PM

Github Classroom Gradescope
In this practical you will extend the color picker to obtain additional information from https://www.thecolorapi.com, a publicly available API that provides color-related information.
Learning Goals:
  • Extend color picker to fetch additional data from an API
  • Practice using `fetch` and Promises
Submission: Successfully submitting your assignment is an ordered two-step process:
  1. Commit and push your changes to GitHub
  2. Submit your repository to the Gradescope assignment

Prerequisites

  1. Create the git repository for your practical by accepting the assignment from GitHub Classroom. This will create a new repository for you with a skeleton application already setup for you.
  2. Clone the repository to you computer with đź’» git clone (get the name of the repository from GitHub).
  3. Open up the package.json file and add your name as the author of the package.
  4. Install the module dependencies by executing đź’» npm install in the terminal.

Background

So far our applications have used the build tools to load their data. That is atypical. More commonly our applications fetch their data from a server. In this practical we extend the color picker to obtain additional information from https://www.thecolorapi.com, a publicly available API that provides color-related information. In particular we are interested in its “nearest named color” feature, the returns a “named” color similar to our query. We want to fetch and display that information in the color picker, including enabling the user to switch to the “named” color. To make this feature feel “reactive”, we want to obtain the additional color information automatically as we adjust the color sliders, not just in response to a specific user request.

The color API takes a variety of inputs. Here we will use the RGB interface, specifying the color of interest via a query parameter containing the RGB values. For example, to find out more about “Middlebury Blue”, we would query https://www.thecolorapi.com/id?rgb=(55,83,140). The response is JSON with a variety of data about the color (API documentation). We are interested in the following portion that describes the similar named color.

{
   ...
   "name": {
      "value":"Chambray",
      "closest_named_hex":"#354E8C",
      "exact_match_name":false,
      "distance":51
   }
   ...
}

Taking actions in response to changes in a component

To fetch additional data about any given color, we need to be able to execute a callback that has side effects (e.g., launching a network request) in response to changes in our component. We employ the useEffect hook to do so. useEffect executes a callback every time the component re-renders. We will ultimately use that callback to fetch data from the API and change our component state. useEffect takes an optional second argument, an array of dependencies. React will skip the effect if none of those specific dependencies have changed since the last render. In our context, we would want to fetch new data whenever (and only when) the red, green, or blue state changes. Implement the following hook in your ColorPicker component:

useEffect(() => {
  console.log(`https://www.thecolorapi.com/id?rgb=(${red},${green},${blue})`);
}, [red, green, blue]);

Start the development server, and your browser’s developer tools. You should see new log entries in the console when you move the sliders. I suspect you see a lot of entries. Recall that our sliders are controlled components that change the state and thus trigger a re-render on every change to the slider. That will generate many more requests than we want (and may degrade the API, get us throttled or even banned). While we want the name feature to feel reactive, we don’t need to be quite that reactive. Thus we are going to debounce the effect body to only execute after the input has stabilized for some amount of time, 250 ms in this example.

Debouncing the input

To debounce the input we will use setTimeout. The general idea is delay each update by some time. If there has been additional change within that window we will cancel the previous update and restart the delay. The clearTimeout function cancels the previous timeout call (if still active). Every time the state changes we want to clear any pending timeouts and create a new one. We often need to do this kind of “cleanup”. If your effect callback returns a function, React will invoke the returned function each time the effect runs again and one final time when the component is unmounted (removed from the DOM). Here we return a function that calls clearTimeout on the setTimeout we just launched. If the time has already full elapsed, and the timeout callback executed, nothing happens. But if the timeout is still pending, it will be canceled (effectively debouncing the input).

Update your hook with the following. Now you should only see the logging statements when you have stopped moving the sliders (or otherwise updating the colors) for 250ms.

useEffect(() => {
  const queryTimeout = setTimeout(() => {
    console.log(`https://www.thecolorapi.com/id?rgb=(${red},${green},${blue})`);
  }, 250)
  return () => {
    clearTimeout(queryTimeout);
  }
}, [red, green, blue]);

Rendering the nearest color

We want to display the name of the nearest color and either a 🎯 when our color components exactly match the named color, or if not, a button, labeled “Switch”, to update our color components to the named color. Add the following to your ColorPicker after the sliders. Note the two different forms of conditional rendering (using short-circuit evaluation and the ternary operator).

{name && (
   <div>
     <p>Most similar named color: {name.value} {name.exact_match_name ? "🎯" : <button onClick={updateNearest} type="button">Switch</button>}</p>
  </div>
)}

This snippet implies a piece of state name and a helper function updateNearest (with no arguments) that is invoked when you click on the button. The state will be set with the name object returned by the API (shown above) and updateNearest will parse the RGB hex representation to update the color components. Implement both in your ColorPicker component. For the latter, the parseInt function can be used to translate a string into a number for any base. For example, parseInt("F",16) will translate the hex digit F to the number 15.

Test your newly created UI by “hard coding” an initial value for name using the example data above. You should see Chambray display and be able to switch the color components to that color.

Actually fetching the data

Modern browsers (and now Node) provide a fetch function for performing network requests. fetch returns a Promise that is fulfilled once the response is available. Replace the console.log in your effect hook with the following:

fetch(`https://www.thecolorapi.com/id?rgb=(${red},${green},${blue})`)
.then((response) => {
  if (!response.ok) {
   throw new Error(response.statusText);
  }
  return response.json();
})
.then((response) => {
   setName(response.name);
}).catch(err => console.log(err)); // eslint-disable-line no-console

This requests the data from the color API for current color components. When the fetch promise resolves, we check for a valid response, before parsing the returned JSON. The response.json() method also returns a Promise, which subsumes the Promise originally returned by then and is eventually fulfilled with the parsed objects. The final then updates the component state when that parsed object is available.

Try out your application. After your finish dragging the sliders, the “Most similar named color” should update!

Switching to async and await

As we discussed in class, we can interact with Promises in a more imperative style by using the async and await keywords. The former declares a function to be asynchronous, thus enabling the use of the await keyword. The latter suspends execution until the Promise returned by the following expression is fulfilled or rejected. The use of async and await doesn’t change the behavior of Promises, instead they are intended to simplify the syntax for consuming Promise-based APIs.

Update your effect hook to use async and await instead ofthen. Make the callback provided to setTimeout be aysnc, then replace the callback body with the following. Note the two awaits, one for each expression returning a Promise.

const response = await fetch(`https://www.thecolorapi.com/id?rgb=(${red},${green},${blue})`);
if (response.ok) {
  const colorInfo = await response.json();
  setName(colorInfo.name);
}

Finishing up

Commit any changes you may have made and then push your changes to GitHub. You should then submit your repository to Gradescope.

Grading

Required functionality:

  • Fetch and display most similar named color data
  • Debounce requests to only fetch data after inputs is stabilized for 250 ms
  • Update the color components to named color when “Switch” button clicked
  • Pass all tests
  • Pass all ESLint checks

Recall that the Practical exercises are evaluated as “Satisfactory/Not yet satisfactory”. Your submission will need to implement all of the required functionality (i.e., pass all the tests) to be Satisfactory (2 points).

Read more about useEffect and data fetching

useEffect is one of the trickier built-in hooks and is often overused. Check out the post “You might not need an effect” for some examples of situations where you don’t need and shouldn’t use useEffect. Fetching data from an external API, like we are doing here, is one of the situations where we do need useEffect. Why? Because we are trying to keep our component “synchronized” with an external system, e.g., the color data provided by the API. That article also states:

Keep in mind that modern frameworks provide more efficient built-in data fetching mechanisms than writing Effects directly in your components.

Here we purposely chose a “low-level” approach using useEffect and fetch for pedagogical purposes. You can continue to do the same throughout the semester. But I also encourage you to check out the more sophisticated data fetching hooks available. For example useSWR is provided by the same team that created Next.js. Similarly, we directly implemented the debouncing behavior. But again we could have installed an existing hook for that task. See below for more discussion.

Reusing hooks with hook libraries

One of the motivations for hooks to encapsulate common functionality for reuse, for example debouncing. That is instead of directly incorporating debouncing into the useEffect (and re-implementing it everywhere we need debouncing), we can use a pre-existing hook that abstracts that functionality. For example, the useHooks library provides a useDebounce hook and many more. There are several such libraries (useHooks is just one, and may not be the best or most comprehensive). In our assignments and practicals we purpose implement these features ourselves so we learn about what is going on “behind the scenes”. In your projects I encourage to investigate pre-existing hooks for common tasks.


© Laura Biester, Michael Linderman, and Christopher Andrews 2019-2024.