Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

Controlled Form Input

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I have good news and bad news. The bad news? React has two totally different options for interacting with forms. And nobody likes extra choices. The good news? We are going to learn both, but then I'll tell you exactly which choice I think you should make, and when.

The first option is what we've been doing so far: you render the form field then interact with the DOM element directly to read or set its value. In this world, React is basically unaware that this field exists after it's rendered.

The second option is quite different. When you render the field, you bind its value to a piece of state. Then, instead of working with the DOM element to read and set its value, you read and set that state. And, of course, when you update the state, React re-renders the field with the new value.

In the first approach, the DOM is the source of truth for the value of a field. In the second approach, the state is the source of truth.

Adding the Field State

To show off the second approach, we're going to add a really important new feature: a form field at the top of the page where the user can control the number of hearts they want to see. If they enter 50, we'll heart them 50 times!

In RepLogs, right after the h1, add a simple <input type="number" />.

... lines 1 - 30
return (
<div className="col-md-7">
... lines 33 - 34
<input type="number" />
... lines 36 - 67
</div>
);
... lines 70 - 79

Nothing interesting yet. Yep, hello, new, empty field. Next, we need to know how many hearts the user wants. That means we need some new state. We could put that state inside RepLogs. After all, the number of hearts is a pretty not-important, UI-related state. But, to keep RepLogs simple, let's put it in RepLogApp.

Initialize numberOfHearts to 1. As soon as we do this, thanks to how we're rendering RepLogs, this new state is automatically passed as a prop.

... lines 1 - 6
constructor(props) {
... lines 8 - 9
this.state = {
... lines 11 - 16
numberOfHearts: 1
};
... lines 19 - 21
}
... lines 23 - 57

Awesome! Copy numberOfHearts and head down to add it as a new prop type: numberOfHearts set to PropTypes.number.isRequired.

... lines 1 - 75
RepLogs.propTypes = {
... lines 77 - 81
numberOfHearts: PropTypes.number.isRequired
};

Above, destructure this value out, and then, this looks a little crazy, copy the heart, enter JavaScript, paste the heart in some quotes and .repeat(numberOfHearts).

... lines 1 - 17
const {
... lines 19 - 23
numberOfHearts
} = props;
... lines 26 - 27
if (withHeart) {
heart = <span>{'❤️'.repeat(numberOfHearts)}</span>;
}
... lines 31 - 84

We haven't bound the state to the field yet, but we should already be able to play with this! Refresh the page. One heart. Find the React tools and change the state to 10. Yay!

Binding the Input to State

And this is where the two options diverge. If we were to do things the same as before, we would add an onChange handler to the input, read the value from the DOM directly, and use that to set some state. But, the state and the input wouldn't be connected directly. Oh, and, to be 100% clear, if you want to read a value off of a DOM element directly, you don't necessarily need to use refs. Inside the onChange handler, you could use event.target to get the element.

Refs are just a tool: they're handy if you need to find several fields inside a form, or, in general, whenever you need to work with a DOM element and you don't have access to it.

Anyways, to use the second, state-based approach, literally say value={numberOfHearts}.

... lines 1 - 35
<input
... line 37
value={numberOfHearts}
/>
... lines 40 - 84

Try it! Refresh. And, hey! We see a value of 1! But in the console... a huge error! Wah, wah. Oh, and the field is stuck at 1: I can't change it. The error explains why:

You provided a value prop to a form field without an onChange handler. This will render as a read-only field.

Updating the State

This new strategy - where you set the value of a field to some state - is called a controlled component. React will always make sure that this value is set to the value of this prop... which means that it won't allow us to change it! If we want the value to change, we need to update the underlying state: numberOfHearts in RepLogApp.

To do this, add another handler function: handleHeartChange(). And remember: our top-level component is all about changing state: it shouldn't be aware of, or care, that there is a form input that's used to change this. So, give it just one argument: the new heartCount.

Inside, set the state! this.setState() with numberOfHearts set to heartCount.

... lines 1 - 43
handleHeartChange(heartCount) {
this.setState({
numberOfHearts: heartCount
});
}
... lines 49 - 65

And because we just added a handler function, don't forget to go up to the constructor and add this.handleHeartChange = this.handleHeartChange.bind(this).

... lines 1 - 6
constructor(props) {
... lines 8 - 21
this.handleHeartChange = this.handleHeartChange.bind(this);
}
... lines 24 - 65

Back down in render, all our state and props are automatically passed. The only things we need to pass manually are the handlers: onHeartChange={this.handleHeartChange}.

... lines 1 - 49
render() {
return (
<RepLogs
... lines 53 - 56
onHeartChange={this.handleHeartChange}
/>
)
}
... lines 61 - 65

Finally, open RepLogs and scroll down to propTypes: we're now expect an onHeartChange function that is required. Back up, destructure that new variable.

... lines 1 - 16
export default function RepLogs(props) {
const {
... lines 19 - 24
onHeartChange
} = props;
... lines 27 - 77
}
... line 79
RepLogs.propTypes = {
... lines 81 - 86
onHeartChange: PropTypes.func.isRequired
};

We need to update the state whenever the field changes. This means we need an onChange. Set it to an arrow function with an e argument. Inside, it's so nice: onHeartChange(e.target.value).

... lines 1 - 32
return (
... lines 34 - 36
<input
... lines 38 - 39
onChange={(e) => {
onHeartChange(e.target.value);
}}
/>
... lines 44 - 76
);
... lines 78 - 89

We do reference the DOM element - e.target - but just for a moment so that we can call the handler & update the state.

And... we're done! Let's try it - refresh! Change this to 10. Ha! It works! We are happy!

Casting to an Integer

Oh, except, hmm, we just failed prop validation?

numberOfHearts of type string supplied, expected number.

Interesting. In RepLogs, we expect the numberOfHearts prop to be a number... which makes sense. But apparently, it's now a string! This isn't that important... but it is interesting!

When you read a value from a field, it is, of course, always a string! That means the numberOfHearts state becomes a string and that is passed down as a prop. Let's clean that up: we could do that right here, or inside the handler function. To do it here, oh, this is bizarre, add a + before the variable.

... lines 1 - 39
onChange={(e) => {
onHeartChange(+e.target.value);
}}
... lines 43 - 89

That will change the string to a number. There are other ways to do this - JavaScript is weird - but this is one way.

Try it again: change the value and... no error!

Welcome to the world of "controlled components"! It feels really good... but it can be a bit more work. Don't worry: in a few minutes, we'll talk about when to use this strategy versus the original.

Oh, but to make this a little bit more fun, change this to input type="range".

... lines 1 - 36
<input
type="range"
... lines 39 - 42
/>
... lines 44 - 89

Try it! Super-fun-heart-slider!!!

Next, let's refactor RepLogCreator to use controlled components. This will be the best way to see the difference between each approach.

Leave a comment!

2
Login or Register to join the conversation
Benoit D. Avatar
Benoit D. Avatar Benoit D. | posted 1 year ago | edited

Instead of casting to an integer, I'd suggest to use valueAsNumber instead of value:
`
<input type="number" onChange={(e) => {

    onHeartChange(e.target.valueAsNumber)

}}/>
`

Reply

Hey Benoit D. !

Ha! I somehow never knew about that property - I love it! Thanks for sharing.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// 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
    }
}

What JavaScript libraries does this tutorial use?

// 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
    }
}
userVoice