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 SubscribeWith controlled components, the value for each field in your form needs to be set to state. And then, you need to make sure to update that state whenever the field changes. But once you do this, everything else is super nice! The input automatically renders with the correct value, and it's dead-simple to read that state and use it in other places.
In RepLogCreator
, we did not use this strategy. Nope, we took advantage of refs
to access the DOM elements directly, and then read the values from there.
To really compare these two approaches, let's see how it would look to use "controlled components" inside of RepLogCreator
. Then, later, I'll give you my clear recommendation on when to use each.
Copy RepLogCreator
and create a new file: RepLogCreatorControlledComponents.js
. Next, in RepLogs
, copy the import statement, comment it out and, instead, import RepLogCreator
from this new file.
... lines 1 - 3 | |
//import RepLogCreator from './RepLogCreator'; | |
import RepLogCreator from './RepLogCreatorControlledComponents'; | |
... lines 6 - 90 |
... lines 1 - 3 | |
export default class RepLogCreator extends Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
quantityInputError: '' | |
}; | |
this.quantityInput = React.createRef(); | |
this.itemSelect = React.createRef(); | |
this.itemOptions = [ | |
{ id: 'cat', text: 'Cat' }, | |
{ id: 'fat_cat', text: 'Big Fat Cat' }, | |
{ id: 'laptop', text: 'My Laptop' }, | |
{ id: 'coffee_cup', text: 'Coffee Cup' }, | |
]; | |
this.handleFormSubmit = this.handleFormSubmit.bind(this); | |
} | |
... lines 24 - 94 | |
} | |
... lines 96 - 100 |
Perfect! Because our form has two fields - the select & the input - we need two new pieces of state. On top, add selectedItemId
set to empty quotes and quantityValue
set to 0. Delete the old refs stuff.
... lines 1 - 4 | |
constructor(props) { | |
... lines 6 - 7 | |
this.state = { | |
selectedItemId: '', | |
quantityValue: 0, | |
... line 11 | |
}; | |
... lines 13 - 21 | |
} | |
... lines 23 - 99 |
In render()
destructure these out of state, and use them below: instead of ref=
, use value={selectedItemId}
. On the input, the same thing: value={quantityValue}
.
... lines 1 - 51 | |
render() { | |
const { quantityInputError, selectedItemId, quantityValue } = this.state; | |
... line 54 | |
return ( | |
... lines 56 - 61 | |
<select id="rep_log_item" | |
value={selectedItemId} | |
... lines 64 - 79 | |
<input type="number" id="rep_log_reps" | |
value={quantityValue} | |
... lines 82 - 91 | |
); | |
} | |
... lines 94 - 99 |
Oh, this is cool: when you use a controlled component with a select element, you add the value=
to the select element itself! That's not how HTML works. Normally, you need to add a selected
attribute to the correct option
. But in React, you can pretend like the select itself holds the value. It's pretty nice.
As soon as you bind the value of a field to state, you must add an onChange handler. Above, create handleSelectedItemChange()
with an event argument. Inside, all we need to do is set state: this.setState()
with selectedItemId
set to event.target.value
. event.target
gives us the select DOM element, and then we use .value
. We don't need to read the selectedIndex
like before.
... lines 1 - 51 | |
handleSelectedItemChange(event) { | |
this.setState({ | |
selectedItemId: event.target.value | |
}); | |
} | |
... lines 57 - 113 |
Copy this function, paste, and call it handleQuantityInputChange
. This time, update quantityValue
... but the event.target.value
part can stay the same. Nice!
... lines 1 - 57 | |
handleQuantityInputChange(event) { | |
this.setState({ | |
quantityValue: event.target.value | |
}); | |
} | |
... lines 63 - 113 |
Before we use these functions in render, head up to the constructor and bind both of them to this.
Finally, head back down to hook up the handlers: onChange={this.handleSelectedItemChange}
and for the input, onChange={this.handleQuantityInputChange}
.
... lines 1 - 63 | |
render() { | |
... lines 65 - 66 | |
return ( | |
... lines 68 - 73 | |
<select id="rep_log_item" | |
... line 75 | |
onChange={this.handleSelectedItemChange} | |
... lines 77 - 92 | |
<input type="number" id="rep_log_reps" | |
... line 94 | |
onChange={this.handleQuantityInputChange} | |
... lines 96 - 105 | |
); | |
} | |
... lines 108 - 113 |
Ok: the controlled components are setup! Move over, refresh, inspect element to find the text input, click it, and then go over to React. The dev tools show us this exact element... which is nice because we can scroll up to find RepLogCreator
and see its state!
Select a new item. New state! Change the input. New state again!
The hard work is now behind us. Find handleFormSubmit()
. Instead of looking at the DOM elements themselves... we can just read the state! On top, destructure what we need: const { selectedItemId, quantityValue } = this.state
. Delete the old refs stuff.
... lines 1 - 25 | |
handleFormSubmit(event) { | |
... lines 27 - 28 | |
const { selectedItemId, quantityValue } = this.state; | |
... lines 30 - 49 | |
} | |
... lines 51 - 113 |
Then, in the if statement, it's just if quantityValue
. That is nice.
... lines 1 - 25 | |
handleFormSubmit(event) { | |
... lines 27 - 30 | |
if (quantityValue <= 0) { | |
... lines 32 - 37 | |
} | |
... lines 39 - 49 | |
} | |
... lines 51 - 113 |
Use that again below for onAddRepLog
. For the first argument, put a TODO just for a minute. Then, at the bottom, clearing the form fields is also easier: delete the old code, then re-set the selectedItemId
and quantityValue
state back to their original values.
... lines 1 - 39 | |
onAddRepLog( | |
'TODO - just wait a second!', | |
quantityValue | |
); | |
... line 44 | |
this.setState({ | |
selectedItemId: '', | |
quantityValue: 0, | |
... line 48 | |
}); | |
... lines 50 - 113 |
Ok, back to that onAddRepLog()
call. The first argument is the item label: that's the visual part of the option, not its value. But our state - selectedItemId
is the value. We're going to change this to use the value later, once we introduce some AJAX. But, thanks to the itemOptions
property we created earlier, we can use the option id to find the text. I'll create a new itemLabel
variable and paste in some code. This is super not important: it just finds the item by id, and, at the end, we call .text
to get that property.
... lines 1 - 30 | |
const itemLabel = this.itemOptions.find((option) => { | |
return option.id === this.state.selectedItemId | |
}).text; | |
... lines 34 - 117 |
Use that below: itemLabel
.
... lines 1 - 43 | |
onAddRepLog( | |
itemLabel, | |
... line 46 | |
); | |
... lines 48 - 117 |
And... I think we're ready! Move over and refresh. Lift our big fat cat 25 times. We got it! Try some coffee while we're at it.
Ok, let's finally judge these two approaches. The old RepLogCreator
uses the first strategy, called "Uncontrolled Components". It's about 100 lines long. RepLogCreatorControlledComponents
is a bit longer: 116 lines. And that reflects the fact that controlled components require more setup: each field needs its own state and a handler to update state. Sure, there are some clever ways to make one handler that can update everything. But, the point is, the added state and handlers means a bit more setup & complexity. On the bright side, when you need to read or update those values, it's super easy! Just use the state. Oh, even this is too much code: I forgot to use the local selectedItemId
variable.
Controlled components are the React officially-recommended approach to forms. However, because of the added complexity & state, we recommend using "uncontrolled components" instead... most of the time. But, this is subjective, and you'll be fine either way. No decision is permanent, and switching from uncontrolled components to controlled is easy.
So, when do we recommend controlled components? The biggest time is when you want to do render something as soon as a field changes - not just on submit. For example, if you wanted to validate a field as the user is typing, disable or enable the submit button as the user is typing or reformat a field - like a phone number field... once again... as the user is typing. This is why the heartCount
input was perfect as a controlled component: we want to re-render the hearts immediately as the field changes.
If you're not in one of these situations, you can totally still use controlled components! But we usually prefer uncontrolled components.
Oh, and remember another downside to controlled components is that they do require your component to have state. And so, if your dumb component is a function, like RepLogs
, you'll need to refactor it to a class. No huge deal - just something to think about.
In RepLogs
, let's change the import to use our original component.
"Houston: no signs of life"
Start the conversation!
// 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
}
}