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 SubscribeEarlier, we talked about the three types of validation. First, HTML5 validation with things like the required
attribute. It's dead-simple to setup, but limited. Second, custom client-side validation, which we added because we wanted to make sure the users enter a positive quantity. And third, of course, the one type of validation you must have: server-side validation.
Look at the RepLog
entity. We already have a few important validation constraints: reps
cannot be blank and needs to be a positive number, and the item
also cannot be blank.
Thanks to HTML5 validation & client-side validation, we are already preventing these bad values from even being submitted to the server. And, of course, if some annoying hacker wants to send bad values to the API, sure, they totally could. But then our server-side validation would be there to tell them to bugger off.
However, a lot of time, I either skip client-side validation entirely, or just add it for a few things, but not everything. And, in that case, if an API request fails because of failed server-side validation, our React app needs to read those errors from the server and tell the user.
Check out RepLogController
. We're using the form system, but that's not important. Nope, the really important thing is that we, somehow, get a RepLog
object that's populated with data, and run it through the validation system. The form does this for us. But if you were manually setting up the object or using the serializer to deserialize, you could pass the object directly to the validation system to get back a collection of errors.
In this application, I added a shortcut method called getErrorsFromForm
, which lives in BaseController
. This recursively loops over my errors to create a big array of errors, where the key is the name of the field. This is what's returned from our API.
When you use the form system, there is one other way to add validation, which is often forgotten: on the form itself: RepLogType
. The ChoiceType
is normally used to render a select
element where choices
is where you define the valid options. When used in an API, if we submit a value that is not in choices, the form will fail validation.
For testing purposes, let's purposely make this easy to do. In RepLogCreator
, find the itemOptions
array: these items match what's configured inside the form. Add a fake one: invalid_item
with text Dark Matter
.
... lines 1 - 4 | |
constructor(props) { | |
... lines 6 - 14 | |
this.itemOptions = [ | |
... lines 16 - 19 | |
{ id: 'invalid_item', text: 'Dark Matter' } | |
]; | |
... lines 22 - 23 | |
} | |
... lines 25 - 101 |
The server will not like this value. Let's try it anyways! Move over, select "Dark Matter", 10 and... ah! Run! Woh!
Ok, two things. First, you can see the request failed: 400 bad request. Great! Our server-side validation is working and you can see the message in the response. But, second, React exploded in a crazy way! Something about how each child in an array should have a unique "key" prop, from RepLogList
.
We know that error... but why is it suddenly happening?
There's one simple explanation and it's hiding in rep_log_api.js
. If you used jQuery's AJAX function, you might remember that if the server returns an error status code, a failure callback is executed instead of your .then()
, success callback. That makes sense: the request failed!
But... fetch()
does not do this. Even if the server sends back a 400 or 500 error... fetch thinks:
We did it! We made a request! Yaaay! Let's execute the
.then()
success callbacks!
Thanks to that, our app parsed the JSON, thought it contained a rep log, tried to add it to state, and things went bananas.
This behavior... isn't great. So, I like to fix it: I'll paste in a new function called checkStatus()
. If we call this function and the status code is not 200 or 300, it creates an Error
object, puts the response onto it, and throws it. By the way, you could change this logic to also throw an error for 300-level status codes, that's actually how jQuery works.
... lines 1 - 12 | |
function checkStatus(response) { | |
if (response.status >= 200 && response.status < 400) { | |
return response; | |
} | |
const error = new Error(response.statusText); | |
error.response = response; | |
throw error | |
} | |
... lines 23 - 48 |
To use this, back up in fetchJson()
, add this handler: .then(checkStatus)
.
function fetchJson(url, options) { | |
return fetch(url, Object.assign({ | |
... line 3 | |
}, options)) | |
.then(checkStatus) | |
.then(response => { | |
... lines 7 - 9 | |
}); | |
} | |
... lines 12 - 48 |
Let's try it! Refresh, select our bad item, a number and... yes! This is a much more obvious message:
Uncaught (in promise) Error: Bad Request at
checkStatus
Now that fetch
is behaving better, let's use this error response to add a message for the user.
Hey Milan V.
We're glad to hear that you are enjoying our tutorials :)
Tomorrow morning will be released the next chapter!
Hi Guys,
1st, Thank you for making these tutorials. They really help me get a leg up on real world projects and my bosses are happy!
2nd, I am getting an error but not exactly what we're expecting. I expected the error to be at the throw line. But, in my <i>sources->rep_log_api.js tab</i>, chrome is angry at this line:const error = <u><b>new Error(response.statusText);</b></u>
and this is in my console:
`
rep_log_api.js:17 Uncaught (in promise) Error
at checkStatus (rep_log_api.js:17)
at rep_log_api.js:5`
In your example, it should say Error: Bad Request
and also Fetch failed loading: Post "https://localhost:8000/reps"
But mine does not. In the next video, console.log(error)
does not give me an object.
Is the problem with the <i>Error object?</i>
Nevermind, I answered myself. I think it's the difference between mac and windows. Also, in the next video, I did a typo. It's supposed to be console.log(error.response)
. So, my bad ;)
// 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
}
}
Can't wait for the next episode. Really enjoing this so far!