Practical: Working with an API
Initial Due Date: 2024-10-08 11:59PM Final Due Date: 2024-10-25 4:00PMGithub 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
- Commit and push your changes to GitHub
- Submit your repository to the Gradescope assignment
Prerequisites
- 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.
- Clone the repository to you computer with
đź’» git clone
(get the name of the repository from GitHub). - Open up the
package.json
file and add your name as the author of the package. - 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 await
s, 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.