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 SubscribeWe did all the hard work in the beginning: setting up our components and passing around state & callbacks. So now that it's time to make our React app talk to an API, dang, life is fun!
Let's hook up the delete link to our API next. On RepLogController
, we already have an endpoint for this: a DELETE method to /reps/{id}
.
Symfony queries for the RepLog
entity object and we delete it. Oh, and then we return an empty Response.
In JavaScript, find rep_log_api.js
: this is our home for all API requests related to rep logs. Create a second function: export function deleteRepLog()
with an id
argument. Let's cheat and copy the code from getRepLogs()
. But, for the URL, use ticks and say /reps/${id}
.
... lines 1 - 14 | |
export function deleteRepLog(id) { | |
return fetch(`/reps/${id}`, { | |
credentials: 'same-origin', | |
... line 18 | |
}); | |
} |
If you're a hardcore Symfony user... you might hate this! We're hardcoding our URLs! Ah! In Symfony, we never do this. Nope, we always generate a URL by using its route - like with the path()
function in Twig.
When you're working in React - or inside any JavaScript - you have two options when it comes to URLs. Either, (A) hardcode the URLs like I'm doing or (B) somehow generate them dynamically. To generate them, you could use FOSJsRoutingBundle, which is a great option, or set them to a JavaScript variable in Twig and pass them as props. You'll learn how to pass data from Twig to JavaScript later.
But honestly, hardcoding URLs in JavaScript is fine. Your API and your JavaScript are partners: they work together. And that means, if you change something in your API, like a URL - or even a field name - you need to realize that something will probably also need to change in JavaScript. As long as you keep this in mind, it's no big deal. It's even less of a big deal because we're organizing all of our API calls into one spot.
Anyways, the other change is that we need to make a DELETE
request. Do that with another option: method: 'DELETE'
.
... lines 1 - 15 | |
return fetch(`/reps/${id}`, { | |
... line 17 | |
method: 'DELETE' | |
}); | |
... lines 20 - 21 |
Alright! Back to RepLogApp
to put this in action! When a rep log is deleted, handleDeleteRepLog
is called and that removes it from state. Now, we also need to call our endpoint. Head to the top and also import deleteRepLog
. Down below, do it: deleteRepLog(id)
.
... lines 1 - 4 | |
import { getRepLogs, deleteRepLog } from '../api/rep_log_api'; | |
... line 6 | |
export default class RepLogApp extends Component { | |
... lines 8 - 58 | |
handleDeleteRepLog(id) { | |
deleteRepLog(id); | |
... lines 61 - 68 | |
} | |
... lines 70 - 82 | |
} | |
... lines 84 - 87 |
That, is, nice! Try it: move over, refresh and... click delete! Check it out!
Fetch loading finished: DELETE /reps/27
I think it worked! Because this "fetch" call was successful, you can see it under the XHR filter. To make sure it really deleted: refresh. Yep! Just these 3 brave rep logs remain.
I want to point something out: notice that we start the AJAX request, but then immediately update the state... even before it finishes. This is called an "optimistic UI update": it's where you update your state & UI before your server actually saves or deletes the data.
I think this is great, but in some situations, you might want to wait to update the state, until the AJAX call finishes. For example, if the AJAX call might fail due to some failed validation. We'll talk more about that later.
But first, it's time to centralize some code! In rep_log_api.js
, we're starting to repeat ourselves! We now have credentials: 'same-origin'
in two places. That may not seem like a big deal. But, if you were sending an API token and always needed to set a header, centralizing this code would be super important.
Let's create a new utility function that everything else will use. At the top of the file, create a function called fetchJson()
with the two arguments fetch needs: the URL and options. Inside, return fetch()
, the URL, and, for the options, use Object.assign()
passing it an object with credentials
set to same-origin
, comma, options
.
function fetchJson(url, options) { | |
return fetch(url, Object.assign({ | |
credentials: 'same-origin', | |
}, options)) | |
... lines 5 - 7 | |
} | |
... lines 9 - 25 |
Object.assign()
is JavaScript's equivalent of array_merge()
when dealing with objects: it takes any options we might pass in and merges them into this object. So, credentials
will always be in the final options.
Then, because every endpoint will return JSON, we can .then()
to transform the Promise data from the response
object into JSON.
Tip
This introduces a bug when the response is null. We'll handle it in chapter 35 (https://symfonycasts.com/screencast/reactjs/deep-state-update)
... lines 1 - 4 | |
.then(response => { | |
return response.json(); | |
}); | |
... lines 8 - 25 |
And just like that, we have a nice utility function that will set our credentials and JSON-decode the response. We're awesome! In getRepLogs()
, simplify: fetchJson('/reps')
. To only return the items
key, add .then(data => data.items)
. This function now returns the same thing as before.
... lines 1 - 14 | |
export function getRepLogs() { | |
return fetchJson('/reps') | |
.then(data => data.items); | |
} | |
... lines 19 - 25 |
For deleteRepLog()
, use fetchJson()
and remove the credentials
key.
... lines 1 - 19 | |
export function deleteRepLog(id) { | |
return fetchJson(`/reps/${id}`, { | |
method: 'DELETE' | |
}); | |
} |
Ok, try it out! Refresh! Yep! Everything works fine. Time to connect our form with the rep log create API endoint.
Hey! Sorry for my slow reply - you just made my night! I love when things totally click in :D
Cheers!
One slight issue - The delete endpoint returns null and a status of 204. The fetchJson function is not happy with the null response when it tries to decode the json. It works, but there is an error in the console. I handled it in my code with return (response.status == 200) ? response.json() : [];
Hey Amy anuszewski
Nice catch! We will discuss about it and come up with the proper solution, thanks for informing us about it!
Have a nice day.
Could go with something like this... just catching the Json Promise and returning a (completely unnecessary for the rest of this) message
function fetchJson(url, options = {}) {
return fetch(
url,
Object.assign({credentials: 'same-origin'}, options)
).then(response => (
response.json().catch((error) => ({'message': 'No Valid JSON returned'}))
));
}
Hey GDIBass
In this case an empty response does not necessarily means an error (the delete action returns null on success). Actually, I think Ryan already knew that and left that task for a further episode, check a fancier solution here: https://knpuniversity.com/s...
Ping GDIBass
Cheers!
// 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
}
}
this tutorial is awesome, the whole thing but right here it really hit me