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).

Handling a Form Submit

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

Hey! Our repLogs live in state! And so, I think it's finally time to add some magic to our form and get it functional. Here's our next goal: when the user submits this form, we want to take its data and update the repLogs state so that a new row is rendered in the table.

The form itself lives in RepLogs, near the bottom. But, the state we need to modify lives in our parent: RepLogApp. To communicate back up the tree, we'll follow a familiar pattern: pass a callback from parent to child, just like we did with onRowClick.

Adding the onSubmit Behavior

Start in RepLogApp: add the handler function: handleNewItemSubmit() with an event object. To prevent the form from actually trying to submit, use event.preventDefault(), just like normal JavaScript.

... lines 1 - 4
export default class RepLogApp extends Component {
... lines 6 - 24
handleNewItemSubmit(event) {
... lines 27 - 29
... lines 31 - 41
... lines 43 - 46

For now, log some stuff! I love when a good form submits! Oh, and also log event.target. Because this function will handle the form element's submit, event.target will be the form itself. We're going to need that so we can read the values from its fields.

... lines 1 - 24
handleNewItemSubmit(event) {
... lines 26 - 27
console.log('I love when a good form submits!');
... lines 31 - 46

Pass this callback as a new prop: onNewItemSubmit = {this.handleNewItemSubmit}. And, hey! We're starting to see a naming convention. This isn't anything official, but I like to name my methods "handleSomeEvent" and my props "onSomeEvent".

... lines 1 - 31
render() {
return (
... lines 35 - 37
... lines 42 - 46

In RepLogs, head straight down to propTypes to describe the prop: onNewItemSubmit is a required function.

... lines 1 - 90
RepLogs.propTypes = {
... lines 92 - 94
onNewItemSubmit: PropTypes.func.isRequired,
... line 96

Love it! Back in render, destructure this into a variable. So: how can we attach a "submit" listener to the form? Ah... it's just onSubmit={onNewItemSubmit}.

... lines 1 - 15
export default function RepLogs(props) {
const { withHeart, highlightedRowId, onRowClick, repLogs, onNewItemSubmit } = props;
... lines 18 - 23
return (
... lines 25 - 51
<form className="form-inline" onSubmit={onNewItemSubmit}>
... lines 53 - 87
... lines 90 - 98

So simple! Go over to the browser and give it a nice refresh! Select an item... fill in a number and... we got it! Every time we submit by pressing enter or clicking the button, we see our insightful message. And as promised, the event.target that we're logging is literally the raw, form DOM element.

This is actually really nice. React always guarantees that event.target will be the element that you attached the listener to.

Reading the Form Data

Next question! How can we read the values from our fields? Look at the form in RepLogs: there's the select element and... the text area. Check it out: it has a name attribute: reps. We can use that and normal, boring JavaScript to find that field and get its value.

By the way... if you've read a little bit about forms and React, this might not be what you were expecting. Don't worry. I'm going to show you a few different ways to get the values from form fields, including the pros and cons of each, and which method I recommend and when.

But right now, forget about React, and remember that, under the hood, there is a boring HTML form sitting on the page that we can interact with.

In RepLogApp, it's time to flex our native JavaScript muscles! To read the reps textarea, use event.target - that's the form - .elements.namedItem('reps'). This will give us the text element. Reads its value with .value.

... lines 1 - 24
handleNewItemSubmit(event) {
... lines 26 - 28
... lines 31 - 46

Let's go try it! Move over, refresh... select "My Laptop" and lift it 50 times. Yes! There's the 50! Victory!

Keep your Smart Component Unaware of Markup

But, before we go further, I need to ask an important philosophical question:

If your shirt isn't tucked into your pants, are your pants tucked into your shirt?

Hmm. Thought provoking. And also: if our smart component - RepLogApp - should not be responsible for rendering any HTML, should its handleNewItemSubmit() method be aware that there is an HTML form and a field with a name="reps" attribute inside?

Actually... no! It makes no sense for handleNewItemSubmit() to suddenly be aware of a specific HTML structure that's rendered by its child. In fact, all RepLogApp should care about is that, when - somehow - a new rep log is created in the app, its handleNewItemSubmit() function is called so that it can update the repLogs state. If it's created with a form, or with some random fields during a 10-step process or just with black magic... RepLogApp should not care!

So, check this out: copy the inside of the function: I'm going to move most of this callback into RepLogs as a new handler function. Inside render(), add a new function: handleFormSubmit() with our normal event argument. Then, paste the logic.

... lines 1 - 23
function handleFormSubmit(event) {
console.log('I love when a good form submits!');
... lines 30 - 105

Down in onSubmit, instead of calling the parent handler, call the new function: handleFormSubmit.

... lines 1 - 30
return (
... lines 32 - 58
<form className="form-inline" onSubmit={handleFormSubmit}>
... lines 60 - 94
... lines 96 - 105

Yep, this feels much better. handleFormSubmit() is responsible for calling event.preventDefault() and uses the form structure - which is created right inside this component - to read the names of the fields. Finally, at the bottom, call the parent handler: onNewItemSubmit().

... lines 1 - 23
function handleFormSubmit(event) {
... lines 25 - 29
onNewItemSubmit('Big Fat Cat', event.target.elements.namedItem('reps').value);
... lines 32 - 107

Actually, this is the reason why I put the new function inside of render() instead of above the function like I did with calculateTotalWeightFancier(): our callback needs access to the props.

Here's the last important part: instead of passing the event object or the form element to the parent onNewItemSubmit() callback, only pass it what it needs: the new rep log's raw data. For now, hardcode an item name - "Big fat cat" - but copy the number of true rep logs and paste.

... lines 1 - 24
function handleFormSubmit(event) {
... lines 26 - 30
onNewItemSubmit('Big Fat Cat', event.target.elements.namedItem('reps').value);
... lines 33 - 110

Back in RepLogApp, clear out handleNewItemSubmit and give it two fresh args: itemName and reps. Log a todo below: we will eventually use this to update the state. And log those values so we can check things!

... lines 1 - 24
handleNewItemSubmit(itemName, reps) {
console.log('TODO - handle this new data');
console.log(itemName, reps);
... lines 29 - 44

I love it! RepLogApp still has a callback, but it's now unaware of the form. It doesn't care how rep logs are created, it only cares that its callback is executed when that happens. All the form logic is right where it should be.

Try it out! Refresh the page, select an item, enter 45 and... submit! The Big fat cat is hardcoded, but the 45 is our real data.

As simple as it is to read the values of the fields by using the name attribute, you probably won't do this in practice. Instead, we'll learn two other ways: refs & state. We'll jump into refs next.

Leave a comment!

Login or Register to join the conversation
CloudCreators Avatar
CloudCreators Avatar CloudCreators | posted 2 years ago | edited

The console.log is showing the reps but just for like a second and it goes away. the Highlighed row id is changing when I click on a row but the color doesnt change it was happening in the start but now it doesnt work.

// RepLogList.js

import React from 'react';
import PropTypes from 'prop-types';  
export default function RepLogList(props){

    const {highlightedRowId , onRowClick , repLogs} = props; 

RepLogList.propTypes = {
    highlightedRowId: PropTypes.any,
    onRowClick: PropTypes.func.isRequired,
    repLogs : PropTypes.array.isRequired,


import React,{Component} from 'react';
import RepLogs from './RepLogs';
import PropTypes from 'prop-types';

export default class RepLogApp extends Component{

    this.state = {
        highlightedRowId : null,
        repLogs : [
            { id: 1, reps: 25, itemLabel: 'My Laptop', totalWeightLifted: 112.5 },
            { id: 2, reps: 10, itemLabel: 'Big Fat Cat', totalWeightLifted: 180 },
            { id: 8, reps: 4, itemLabel: 'Big Fat Cat', totalWeightLifted: 72 }
    this.handleRowClick = this.handleRowClick.bind(this);


            onRowClick = {this.handleRowClick}
            onNewItemSubmit = {this.handleNewItemSubmit}


RepLogApp.propTypes = {

withHeart: PropTypes.bool,


I dont know what is wrong but the color should change I applied the classname info.

Hey CloudCreators!

Sorry for the VERY slow reply - you sent this *just* as I was leaving for a holiday, and nobody else on the team was sure of a good answer for you :).

There is one thing you wrote that I think is really interesting - and could be a clue:

> The console.log is showing the reps but just for like a second and it goes away

Under normal circumstances, I can't think of any reason that a message would disappear from the console log... *except* if the page were performing a full page refresh/navigation. Is it possible that something is navigating the page? So, you see the console.log() for 1 moment, but that click also triggers a navigation. And so, about .5 seconds later, the page navigates away (or, more likely, it reloads) and the console clears. One way to test this is to click the "gear" icon on the upper right of the console area, and check "preserve log". Then, the log will stay even after a refresh. If that makes the log NOT disappear, then it confirms that something is navigating the whole page.

Let me know if that helps out :).


CloudCreators Avatar
CloudCreators Avatar CloudCreators | weaverryan | posted 2 years ago

Yes I clicked the gear and preserved log and it works but idk why it was reloading again and again.


Hey CloudCreators!

Sorry for the slow reply! If it's working now and you can't repeat the problem, I wouldn't worry about it. Perhaps something was going weird with the Webpack build process :).


Mouad E. Avatar
Mouad E. Avatar Mouad E. | posted 5 years ago

why you didn't add handleNewItemSubmit to the constructor and bind it like
this.handleRowClick = this.handleRowClick.bind(this);


Hey Mouerr,

Good question! But the answer is simple: we just don't need it for now. You need to understand why we call that .bind(this). Actually, we'll do it later in https://symfonycasts.com/sc... , but for now that binding context is not important because we just call console.log() inside, i.e. don't use "this" at all.


1 Reply
Mouad E. Avatar

Hi Victor,
Thanks for your support.

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