Promises Practice
Learning Goals
- Review these concepts:
- single-threaded JS
- asynchronous JS
- event loop
-
Utilize Postman as a tool for making requests to APIs
- Utilize
fetch
to get data from an API and utilize the data in a FE app
Warm Up
In your journal, write your answers to the following questions. It is totally OK (and encouraged!) to look back at the notes you took yesterday!
- What is the difference between synchronous and asynchronous JS?
- What are the different statuses of a Promise?
- What is returned when we use the
fetch
API?
What We Will Be Building
— –>
Promises Practice Repo
We’re going to be building out this site with two different repos. One for the client side code and another is an API that serves up a collection of members. Something to note is that the API given to us doesn’t automatically give us all of the info needed to display the staff members. There is a second nested endpoint we will need to fetch… More on this in a moment.
Clone this repo and the promises-api repo down.
Have these directories open in two separate terminal tabs so you can see them both at the same time. For both, run:
npm install
npm start
Postman
Postman is a great tool for making requests to an API and seeing the response you get back in a quick, easy-to-use interface! We will be utilizing this tool throughout Mod 3 and beyond!
Download the app now and get it running now!
Getting Started
Since we are going to be getting our data from an API, it’s usually a good idea to get a sense of what our responses look like when we make a request to a certain endpoint!
You Do
- Make sure you have the
Promises-API
running on port 3001 - With a partner, open Postman and make a
GET
request to the following endpoint:http://localhost:3001/api/frontend-staff
- What is included in the response?
- Try entering one of the endpoints found in the
info
key for one of the objects. What is included in the response?
As you may be starting to see, there is a lot of data available to us but it is going to be a little tricky to access this data. We are going to work together to break down how to access this data to get our app running.
Step 1 - Getting the Initial Bio
Info
Before we can get any of the detailed info for each staff member, we have to start at the initial endpoint as our “entry” into the rest of the dataset. We will be utilizing the fetch API
to access this data!
If you’re not feeling totally comfortable with fetch
yet, I suggest taking a five minutes to review the docs.
Fetch Details
- Fetch returns a Promise, which will either
resolve
orreject
depending on the status of the promise. - You might want to take a look at when fetch actually catches errors here. The API can actually be set up in a way that can help fix this, but this is a major reason why some people dislike
fetch
. - Since
fetch
returns a promise, it makes sense that you can chain.then()
or.catch()
to it.
You Do
Take 5 minutes to read this article about where we should kick off these type of network requests within a React app. Be prepared to discuss the best approach for this problem!
How/Where Will We Fetch?
As you read, utilizing the componentDidMount()
method is the best place to kick off our network request to the first endpoint!
OK, so let’s start by making our initial fetch request and seeing what we get back. See if you can…
- Use
fetch
withincomponentDidMount()
to make a request tohttp://localhost:3001/api/frontend-staff
console.log
the data that comes back!
Initial Fetch w/ console.log
Solution
componentDidMount() {
fetch('http://localhost:3001/api/frontend-staff')
//fetch returns another Promise, which resolves to a Response object
//We can call .json() on the Response object to access the data from
//the body of the Response object
//.json() returns another Promise, which is why we need to chain
//a .then to allow it to resolve!
.then(response => response.json())
.then(staffData => {
//let's check what the data even looks like...
console.log('all data', staffData);
console.log('bio data', staffData.bio);
})
}
Step 2 - Accessing the bio
Data
OK, so it looks like we want to dig into the .bio
from the data that gets returned after we run the .json()
method on our initial response object! But, now we have multiple API endpoints that house the data we are really after… woof.
With a Partner
- Discuss a way we could access the data of each member of the staff
- Since we will have to utilize
fetch
for each staff member’s info, what will be returned from thesefetch
requests? - Write some psuedocode for how you could approach this problem!
Possible Psuedocode Pt. I
- Make initial fetch to
http://localhost:3001/api/frontend-staff
incomponentDidMount
- Iterate using
.map()
over the array of staff membersfetch
from each endpoint within the.map
- Since this
.map
is making network requests usingfetch
, the map will return an array of Promises
- Find some way to resolve all of these daggum Promises from the
.map
!
Great! We have a plan! Plans are #tite
. Let’s work on getting the first two steps implemented.
Iterating Over Staff Members Solution Pt. I
componentDidMount() {
fetch('http://localhost:3001/api/frontend-staff')
.then(response => response.json())
.then(staffData => {
//let's just see what we are getting back!
const promises = staffData.bio.map(staffMember => {
return fetch(staffMember.info)
.then(res => res.json())
})
console.log('promises', promises);
})
}
This is what the data looks like in our console:
Alrighty then… We are getting a little bit closer to grabbing all of the data we need. But we still have a little work to do.
Step 3 - Getting the Data We Need
So we have access to two new pieces of data from this fetch
- the bio
and image
. But we still need to have all of the relevant info for each staff member, especially their name
!
You Do
With a partner, see if you can find a way to extract the necessary data from the response from our fetch
of each staff member and find a way to combine it with their name!
Combining Data w/in .map() Solution
componentDidMount() {
fetch('http://localhost:3001/api/frontend-staff')
.then(response => response.json())
.then(staffData => {
const promises = staffData.bio.map(staffMember => {
return fetch(staffMember.info)
.then(res => response.json())
//after we have pulled the data off the response...
.then(info => {
//create an object with the name
return {
name: staffMember.name,
//and the remaining info (bio + image)
...info
//bio: info.bio,
//image: info.image
}
})
})
console.log('promises', promises);
})
}
This is what our console.log looks like:
Hell yeah! We now have the data looking how we want. But the problem is, it’s still a Promise and we can’t work with it yet. Consarnit! And worst of all, now we have this giant array of Promises - how on earth are we going to resolve all of them?!
Research Spike
- The Problem: we have multiple Promises that need to get resolved.
- Spend 5 minutes researching/figuring out how we can resolve multiple Promise objects!
Step 4 - Resolving ALL of the Promises!
Solution to Problem from Research Spike!
Promise.all()
has entered the chat!
Promise.all()
takes an array of promises and returns a single promise that will eitherresolve
when every promise has resolved orreject
with the reason of the first value in the array that reject.- If the promise array resolves completely, the resulting values will be an array of values whose results are ordered by the order of the promises in the original array - regardless of which promises resolve first!
- This allows us to make ensure that all of the data is returned at one time, in the correct order, rather than making several different
fetch
calls which could resolve at different times and hoping for the best!
Dope! So now we have a way to work with all of the Promises returned from our .map
! But before we implement it, let’s check the docs and make sure we understand what we are getting back from this solution!
You Do
- With a partner, see if you can return all of the Promises into a format we can work with by
console.log
the data! Hint: What doesPromise.all
return? - After you can see the data, how could we store the data within the
state
of our App?
Promise.all() Solution
componentDidMount() {
fetch('http://localhost:3001/api/frontend-staff')
.then(response => response.json())
.then(staffData => {
const promises = staffData.bio.map(staffMember => {
return fetch(staffMember.info)
.then(res => res.json())
.then(info => {
return {
name: staffMember.name,
...info
}
})
})
//this will return us a single Promise!
//If it returns a Promise, we need to use .then
//to give it time to resolve!
return Promise.all(promises)
})
//We can now set the workable data to our state!
.then(staff => this.setState({ staff }))
}
Step 5 - Building a Helpers File
Bravo! - we have successfully integrated the nested data from the API into our App’s state! But, this is a pretty burly chunk of code and isn’t super readable. This is where you might find it more beneficial to break this into a separate file and bring in the functionality only when you need it!
You Do
With a partner, see if you can extract the logic from our fetch
call into it’s own function within a new file!
- Create a new file called
helpers.js
- Create a function called
fetchStaffBios
- Think about what logic from the
fetch
from our App needs to be broken out and extract that logic into thefetchStaffBios
function - Import the function into your
<App />
component and use it within thefetch
ofcomponentDidMount
. If the app still works, you did it! - Add some error handling to your
fetch
to handle if something goes awry anywhere along the way!
Final Solution!
// helpers.js
export const fetchStaffBios = staffData => {
const promises = staffData.bio.map(staffMember => {
return fetch(staffMember.info)
.then(res => res.json())
.then(info => {
return {
name: staffMember.name,
...info
}
})
})
return Promise.all(promises)
}
// App,js
class App extends Component {
constructor() {
super();
this.state = {
staff: [],
error: ''
};
}
componentDidMount() {
fetch('http://localhost:3001/api/frontend-staff')
.then(response => response.json())
.then(staffData => fetchStaffBios(staffData))
.then(staff => this.setState({ staff }))
.catch(error => this.setState({ error }))
}
render() {
//JSX
}
}
Resources
- MDN docs
- Loupe, by Philip Roberts, was used for examples.
- DAN MARTENSEN
- Promises, Async/Await