If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen we used fetch()
to make an AJAX call to /reps
... we were "rewarded" with this big ugly error. This tells me that fetch is probably having problems... for some reason... parsing the JSON in the response.
Let's go see what happened! Click on the Network tab in your browser tools and filter for only XHR requests. Ah, here is one for /reps
that was successful. BUT! That's the wrong AJAX call: this is the AJAX call made by our old code. So... where the heck is the other /reps
AJAX call that was just made by fetch()
?
Click instead to filter by the "Other" tab. There it is! Why is it here? Well... because... something went wrong. Look at the response: 302. And if you look at the response headers... woh! It is a redirect to the login page, which is why you see a second request below for /login
.
Let's back up. First, for some reason, authentication is failing for the API request. We'll get to that in a minute. Second, fetch()
requests will normally show up under the XHR network filter. We'll see that later. But, if something goes wrong, the request may show up under "Other". Just be aware of that: it's a gotcha!
So, why the heck is authentication failing? If we go directly to /reps
, it works! What's wrong with you fetch!
This, in my opinion, is one of the really cool things about fetch. Look at our controller. Ah, every endpoint requires us to be logged in! This works in our browser because our browser automatically sends the session cookie. But fetch()
, on the other hand, does not automatically send any cookies when it makes a request.
I like this because it forces you to ask yourself:
Hey, how do I want to authenticate my API requests?
API authentication is a big topic. So we're going to skip it! I'm kidding: it's too important.
One way or another, every API request that needs authentication will have some sort of authentication data attached to it - maybe a session cookie or an API token set on a header.
So... what type of authentication should you use for your API? Honestly, if you're building an API that will be consumed by your own JavaScript front-end, using session cookies is an awesome option! You don't need anything fancier. When we login, that sets a session cookie. In a moment, we'll tell fetch to send that cookie, and everything will be solved. If you want to build your login page in React and send the username and password via AJAX, that's totally fine: when your server sends back the session cookie, your browser will see it & store it. Well, as long as you use the credentials
option that I'm about to show you for that AJAX call.
Of course, if you want, you can also create an API token authentication system, like JWT or OAuth. That's totally fine, but that truly is a separate topic.
Whatever you choose, when it's time to make your API call, you will attach the authentication info to the request: either by sending the session cookie or your API token as a header.
To send the session cookie, fetch has a second options argument. Add credentials
set to same-origin
. Thanks to this, fetch()
will send cookies to any requests made back to our domain.
... lines 1 - 6 | |
return fetch('/reps', { | |
credentials: 'same-origin' | |
}) | |
... lines 10 - 14 |
Tip
The default value of credentials
may change to same-origin
in the future.
Ok, let's see if this fixes things! Move over and refresh. No errors! Check out the console. Yes! There is our data! Notice, the API wraps everything inside an items
key. Yep, inside: the 4 rep logs, which have the same fields as the state in our app. That was no accident: when we added the static data, I made sure it looked like the real data from the API so that we could swap it out later.
In rep_log_api
, I really want my getRepLogs()
API to return a Promise that contains the array of rep logs... without that items
key. To do that, it's a bit weird. The .json()
method returns another Promise. So, to do further processing, chain a .then()
from it and, inside the callback, return data.items
.
... lines 1 - 9 | |
.then(response => { | |
return response.json().then((data) => data.items) | |
}); | |
... lines 13 - 14 |
Promises on top of promises! Yaaaay! When fetch()
finishes, it executes our first callback. Then, when the JSON decode finishes, it executes our second callback, where we read off the .items
key. Ultimately, getRepLogs()
returns a Promise
object where the data is the array of rep logs. Phew!
And because the browser already refreshed while I was explaining all of the promises, yep! You can see the logged data is now the array.
Awesome! Let's use this to set our initial state! First, set the initial repLogs
state to an empty array. Next, copy the getRepLogs()
call and remove it. Instead, create a new method called componentDidMount()
and paste this there. In the callback, use this.setState()
to set repLogs
to data
.
... lines 1 - 22 | |
componentDidMount() { | |
getRepLogs() | |
.then((data) => { | |
this.setState({ | |
repLogs: data | |
}) | |
}); | |
} | |
... lines 31 - 83 |
Before we talk about this, let's try it. Refresh! Woh! We have real data! Yes, yes, yes! We're showing the same data as our original app!
Back to the code! Until this moment, render()
was the only "special", React-specific, method in our class. But there are a few other special methods called "lifecycle" methods. The componentDidMount()
method is one of those: if this exists, React calls it right after our component is rendered to the DOM. And this is the best place to make any AJAX requests needed to populate your initial state.
Actually, we could have left this code in the constructor()
. Because we're in a browser, they're almost the same. But, componentDidMount()
is generally the recommended place.
// composer.json
{
"require": {
"php": "^7.2.0",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^1.6", // 1.9.1
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
"doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
"doctrine/orm": "^2.5", // v2.7.2
"friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
"friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.4
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/monolog-bundle": "^3.1", // v3.3.0
"symfony/polyfill-apcu": "^1.0", // v1.9.0
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/swiftmailer-bundle": "^3.1", // v3.2.3
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/validator": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/twig": "2.10.*" // v2.10.0
},
"require-dev": {
"symfony/debug-pack": "^1.0", // v1.0.6
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.5", // v1.5.0
"symfony/phpunit-bridge": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0" // v4.1.4
}
}
// package.json
{
"dependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
},
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.12.5
"@symfony/webpack-encore": "^0.26.0", // 0.26.0
"babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
"babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
"bootstrap": "3", // 3.3.7
"copy-webpack-plugin": "^4.4.1", // 4.5.1
"core-js": "2", // 1.2.7
"eslint": "^4.19.1", // 4.19.1
"eslint-plugin-react": "^7.8.2", // 7.8.2
"font-awesome": "4", // 4.7.0
"jquery": "^3.3.1", // 3.3.1
"promise-polyfill": "^8.0.0", // 8.0.0
"prop-types": "^15.6.1", // 15.6.1
"react": "^16.3.2", // 16.4.0
"react-dom": "^16.3.2", // 16.4.0
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^7.0.0", // 7.3.1
"sweetalert2": "^7.11.0", // 7.22.0
"uuid": "^3.2.1", // 3.4.0
"webpack-notifier": "^1.5.1", // 1.6.0
"whatwg-fetch": "^2.0.4" // 2.0.4
}
}