Refactor ProductForm.js
Ah forms, a painful Achilles heel of most web applications (React apps included) since the beginning of time.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the The newline Guide to Modernizing an Enterprise React App course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to The newline Guide to Modernizing an Enterprise React App, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:19] Product form is unique in that it's a form component, and form components with multiple inputs have traditionally been difficult and react, more difficult than other JavaScript frameworks might make them. This lesson will walk through converting a react form to use hooks when saving new product info to our local database.
[00:20 - 00:27] I think this refactor might pleasantly surprise you though. With the previous lessons under our belt, we have this well in hand.
[00:28 - 00:50] So let's start where we've traditionally been starting with each of these lessons, converting this class-based component to a function. As you can probably now guess, we are going to take line 20 of our product form , our class declaration, and we're going to make it into an inline arrow function.
[00:51 - 00:57] So const product form becomes a simple inline function. Very good.
[00:58 - 01:17] Of course, the next thing that we're going to do is remove our render from our JSX and remove the extra curly brace. Now, the first thing that we're going to do after that is start replacing our various state variables, and we have a few.
[01:18 - 01:26] We have a new product, which is actually an array of variables that we have defined up here. We have loading, error, saved, and a department's array.
[01:27 - 01:54] So once more, we will remove our React and component imports, bring in our use state hook, and start replacing all of our local state variables. So the first thing that we're going to have is new product and set new product, which we will set equal to our default for new product, which is convenient.
[01:55 - 02:01] We can just set it almost exactly the same way. We do for our class based component.
[02:02 - 02:38] Next up is loading and set loading, which is going to use a state of true, error, and set error, which is going to be a use state of false, saved, and set saved, which is also going to be a use state of false, and finally, departments and set departments, which is going to be a use state of an empty array. Very good.
[02:39 - 02:47] Okay. Now we can remove our constructor from here, and our new use states will handle that.
[02:48 - 02:58] So note that we can still use the defaults for new product right here, but note that we don't have to spread it into an object anymore when we set the state in the component. It's a small change, but it's worth noting.
[02:59 - 03:10] And after this, we will replace our lifecycle method. So we will import a use effect as well, and then we will head over to our component did mount.
[03:11 - 03:23] And this component did mount is going to be replaced with a similar use effect with our callback function, always in view. And we're going to name this function, batch departments.
[03:24 - 03:33] It's going to be an async function, which is why we need it in the first place. And then we will have a new constant that we will call all departments.
[03:34 - 03:47] And we will set that equal to this await get all departments from the department API. And then once again, I will borrow the majority of this.
[03:48 - 04:14] And then we will do a similar check for what is already happening in the component did mount, where if there is a fetch department data error, we will set our error variable to true. And otherwise, we have fetched the departments, so we can set them in our local departments state.
[04:15 - 04:25] And once more, we will set loading down at the very bottom, because either way, error, or good resolution, we're done loading. And then we will actually call that function.
[04:26 - 04:35] And don't forget at the very end to add our empty dependency array so that it will fire off as soon as this component loads. And now we can delete component did mount.
[04:36 - 04:53] It's nice to see that this function can be condensed slightly because each variable is separate, like how we only have to have one set loading as false declaration at the end of the fetch departments function, regardless of the API calls, success or failure. This is a big benefit to react hooks use state variables.
[04:54 - 05:10] It cuts down on the amount of duplicate code, even inside of if else statements and other things like that, because we don't have a single large state object anymore that all has to be updated at once. Instead, each piece of state gets updated independently only when it really needs to be.
[05:11 - 05:22] So this should handle our fetch departments API call, time to move on to some of the other functions in this component. Product form has a number of functions because it is a form component to add new products to the product list page.
[05:23 - 05:29] And we have to handle multiple data inputs for those new products. So we will handle converting these functions one at a time.
[05:30 - 05:45] So the on change function, which we come upon next, only needs slight modifications to work again as an arrow function. Currently, we take the prop name of whichever input is being updated and we set that value in the new product object until all of the properties have been updated.
[05:46 - 05:55] With hooks, we are going to change this to a constant to make it an inline arrow function. Prop name and value will stay the same as the arguments that are passed.
[05:56 - 06:08] And then updated product instead of this dot state dot new product being spread into an object will just become new product being spread into that same object. Updated product prop name equals val gets to stay the same.
[06:09 - 06:22] Instead of this dot set state, we will completely remove all of that and we will just have set new product and wrap update product in it. So that's pretty easy.
[06:23 - 06:38] So now if you were to console log this new product somewhere in our application , you would see the new product object with its newly updated property for whichever input was being interacted with. And don't forget to save the spread operator when copying new product.
[06:39 - 06:51] If you omit it when copying the local copy of new product right here, none of the values in the updated product variable will register as users interact with the input in the DOM. So make sure not to leave it out.
[06:52 - 06:56] Okay, the is valid function that we have next. This one is an easy one to update.
[06:57 - 07:04] It checks each input of the product form to ensure that it's been filled out. And if it hasn't, it keeps the submit button at the bottom of the form from enabling.
[07:05 - 07:20] So all we have to do to make this function work in our new version of product form is throw a const in front of it to make it an arrow function and remove all of the this dot state pieces. Everything else gets to stay the same.
[07:21 - 07:29] Okay, the last function to update is the on submit. So in this function, our new product state is sent to the product API.
[07:30 - 07:45] And if it succeeds in saving a new product to the database or it fails, a toast pops to alert the user to what's happened. Additionally, if it succeeds, the new product object is reset with the empty defaults so that the user can add another product quickly.
[07:46 - 07:56] If it fails, the new product object is not reset in case the user wants to try saving this item again. So we're going to replace our current on submit function with the following code.
[07:57 - 08:05] We will turn this into an arrow function. We will keep everything except we will remove the this dot state to the product API call.
[08:06 - 08:24] We will let this if statement stay intact, but instead we will set the new product back to the defaults for the new product if it succeeds and we will set saved to true. That's all we need to do inside of this if statement.
[08:25 - 08:41] The toast will still pop, that's all good, and then inside of this else statement, we will just go ahead and set our error to true if it fails for whatever reason. And once more at the very end, we will set our loading state to false.
[08:42 - 08:46] Cool. So once again, with the introduction of hooks, we can condense our function.
[08:47 - 08:57] Instead of having to set the loading variable in both instances of whatever the add a new product function returns, we just set it false at the end of the function. So in this particular component, rjsx doesn't even need updating.
[08:58 - 09:04] So I think that we're ready to test the function of our newly refactored component. So let's head over to our browser.
[09:05 - 09:20] The test that our app is working started up locally and navigate to the add new products page. Add a couple of products to the product form component by filling out all of the various things.
[09:21 - 09:39] And let's add a second one. So check when you're filling out this product form that the submit button is disabled until all the form inputs are filled out, which it is.
[09:40 - 09:44] And make sure that the newly added products appear on the product list page. So let's head over here.
[09:45 - 09:51] And if we scroll down, we see our test hammer. And we see our cordless drill.
[09:52 - 09:58] They have shown up very good. I do need to note though that while I was testing this, I did notice an error that I hadn't previously accounted for.
[09:59 - 10:05] Let me show it to you. If I'm in the retail price right here, I had the option to add something besides a number.
[10:06 - 10:16] I can add something like e, e, whatever, and the retail price will suddenly display not a number. We can fix this though.
[10:17 - 10:30] We just need to update our inputs type form from text to number and add a min value of zero so that a user can't set a price below zero because that would just make no sense. So head back over into your IDE for just a second.
[10:31 - 10:40] Let's scroll down to our input for our price, which is right here. And we are going to update this to change it from type of text, type of number.
[10:41 - 10:46] And we're also going to give it a min of zero. Everything else should be good.
[10:47 - 10:52] So now a user should not be able to enter anything besides a number into that input. Okay.
[10:53 - 11:04] So we've just got a few ES-lit errors that we need to address that weren't resolved during the initial refactor. And most of them have to do with an error that gets repeated multiple times.
[11:05 - 11:10] We'll fix this one first. The form label must be associated with a control.
[11:11 - 11:24] If you look at the description online, it says that this shouldn't be an issue because the label component that we're associating with each input on the form has an HTML4 property that matches the inputs ID. And we're doing that.
[11:25 - 11:34] So what's actually going on? Well, it looks like we need to add a new ES-lit rule to allow for form inputs that are not nested within their label.
[11:35 - 11:44] We are going to open the es-lit rc.json file in our project and we are going to add the following snippet. And back to your IDE.
[11:45 - 11:57] Open your es-lit rc.json file. And under our rules, we are going to add the following snippet, which I have copied from the lesson because it's a lot.
[11:58 - 12:08] So just go ahead and grab it and paste it in and save. So this should resolve almost all of our ES-lit errors as soon as it's saved and it takes effect within our IDE.
[12:09 - 12:14] And we already see that our product form errors have gone from 7 to 2. So that's great.
[12:15 - 12:27] And basically all this does is say that even though our label is not wrapped around our input, it is right next to it, but that's still cool. That shouldn't be a linting error.
[12:28 - 12:29] Everything still works as expected. It's still accessible.
[12:30 - 12:38] It's all good. So the other two errors that we have are to remove two unused variables, both error and saved.
[12:39 - 12:53] So when we look at where saved was being set, it was when a new product was successfully added to the database. But because we're using the React Toastify library to alert users when a product has been saved, we don't actually need the saved variable for anything in this particular component.
[12:54 - 12:59] It's not actually doing anything. So let's just delete it completely.
[13:00 - 13:16] And since we have deleted it, we can actually delete the entire state declaration up at the top of our component because that is now no longer being used. One more thing that I need to point out before we get any further is that this department's error needs to now become all departments.
[13:17 - 13:26] Don't forget to do that. I'm going to go ahead and leave this error value for a minute because it's going to play an important role in a second.
[13:27 - 13:34] So just bear with me. Okay, last but not least, let's test the API error states in this component, just like we've done for other components.
[13:35 - 13:54] So head back on over to the browser, open up your Chrome DevTools, head over to your network tab, and go ahead and refresh this page to see all of our API calls. Okay, so the first one that we're looking for is the department API call because that is needed to populate this dropdown.
[13:55 - 14:14] So find it, block it, and refresh the page. So currently when there's an error fetching the department data, an error toast pops, but the form is still present on the page even though a user won't be able to submit this new product because they cannot select a department to classify it under, there's no departments there.
[14:15 - 14:23] So this is a mission critical error for this page. We shouldn't let users waste their time filling out the rest of the form if we can't load the departments for them.
[14:24 - 14:29] So let's fix it in our code so that they can't. Head back over into your terminal.
[14:30 - 14:37] The first thing that we're going to do is remove the toast being triggered in the use effect function. We shouldn't use this any longer.
[14:38 - 14:42] This is not enough of an error for our users to be able to see. So go ahead and delete that toast.
[14:43 - 15:06] And next in the JSX under our loading ternary on line 78, let's add the following error ternary code and a small update to the line right below it so that it renders the form inputs only if there is no error. So we'll use that error variable and we will create a paragraph inside of here.
[15:07 - 15:22] And inside of that, we are going to bring in the fetch department data error plus a little nice at telling them to refresh the page or try again later. Later.
[15:23 - 15:34] Now we're going to give this a class name product form just so it looks a little bit nicer. And then at the end of our ternary, we'll give it a null in case it doesn't need to be displayed.
[15:35 - 15:53] So go ahead and save that and then here on this loading line add an and statement and say if there is not an error, then show the form otherwise don't show it. Now if we can't fetch the department data, we'll alert the user that something went wrong and we'll tell them to try refreshing the page to fix the issue.
[15:54 - 16:01] This makes a lot more sense to me. And as long as there is no error or loading state variables that are set to true, the form will render correctly.
[16:02 - 16:05] Otherwise it won't. So if we look back over in the browser.
[16:06 - 16:15] Alright, here's our final API error test. Check the error if a user can't submit a new product to the product page and we see that something went wrong fetching department data.
[16:16 - 16:22] Please refresh the page or try again. That is a spelling error on my part that I will quickly fix.
[16:23 - 16:39] Okay, so let's unblock this network request and refresh the page once more. Okay, so the other API call that we need to test for errors is the product's API call when we've actually submitted a new product into our database.
[16:40 - 16:47] So find it in the network tab either by creating a new product and submitting it. That's pretty much the best way to do it actually.
[16:48 - 17:07] So find it, block the request URL and then try to create another new test product. And when we submit it, we see that something went wrong fetching department data and something went wrong adding the department to the database.
[17:08 - 17:12] This is not a good user experience. So the same error message is showing up when the product submission fails.
[17:13 - 17:15] This isn't right. Okay, so let's think this through.
[17:16 - 17:22] We've got the error toast popping up when the call fails. Do we really need to set the error state variable to true as well?
[17:23 - 17:27] For what purpose? It doesn't trigger the error toast that's being called separately.
[17:28 - 17:37] I don't think that we really need to set the error to true when that API call fails. So let's head back into our IDE and delete that line from our on submit function.
[17:38 - 17:41] So let's delete this set error right here. Save that change.
[17:42 - 17:48] And back over to the browser. And let's try refreshing one more time.
[17:49 - 18:04] Okay, one more test. And once again, our error toast pops up, but our product form stays intact in case a user wants to try again.
[18:05 - 18:10] Yeah, this makes much more sense. So with that, you're a quick check of our ES lint errors, which are all good.
[18:11 - 18:20] And our testing functionality that's all good, we can say this component is done with its refactoring. We're almost done with the initial conversion of our class components to hooks.
[18:21 - 18:27] Just one more to go. So this last component that we'll be converting in this module is the product list component.
[18:28 - 18:28] See you in the next lesson.