JavaScript for PHP Geeks: ReactJS (with Symfony)


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

Login Subscribe

Right now, we're using the name attribute of each form field to get the underlying DOM element. We use that to fetch its value.

This is interesting: most of the time in React, you communicate down to your elements - and your child components - via props. You use props when you render the element objects, and React handles creating the real DOM elements from that. The DOM is not something we normally touch directly.

But occasionally, you will want to access the underlying DOM elements. For example, you might want to read a value from a form field, call focus() on an element, trigger media playback if you're rendering a video tag or integrate with a third-party JavaScript library that needs you to pass it a DOM element.

Creating the refs

Because these are totally valid use-cases, React gives us a great system to access any DOM element called refs. We need to access two elements: the select element and the input. Cool! In the constructor, create 2 new properties: this.quantityInput = React.createRef() and this.itemSelect = React.createRef().

70 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 4
constructor(props) {
... lines 6 - 7
this.quantityInput = React.createRef();
this.itemSelect = React.createRef();
... lines 10 - 11
... lines 13 - 70

This just, "initialized" these two properties. The real magic is next: on the select, replace the name attribute with ref={this.itemSelect}. Do the same thing on the input: move the props onto their own lines, then add ref={this.quantityInput}.

70 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 24
render() {
return (
... lines 27 - 32
<select id="rep_log_item"
... lines 35 - 51
<input type="number" id="rep_log_reps"
... lines 54 - 62
... lines 65 - 70

To really get what this does, you need to see it. Comment out the onNewItemSubmit() call for a minute: it's temporarily broken. Then, let's console.log(this.quantityInput) and also this.itemSelect.

70 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 13
handleFormSubmit(event) {
... lines 15 - 18
... line 21
//onNewItemSubmit('Big Fat Cat','reps').value);
... lines 24 - 70

Moment of truth! Move over, Encore already refreshed the page. Fill out the fields, hit enter... cool! Each "ref" is an object with one property called current that is set to the underlying DOM element! Yea, I know, the fact that it sets the DOM element to a current key is a little weird... but it's just how it works.

Using the Refs

Thanks to this, let's set the DOM element objects onto two new variables: const quantityInput = this.quantityInput.current and const itemSelect = this.itemSelect.current. Below, log the field values: quantityInput.value and, this is a bit harder, itemSelect.options[itemSelect.selectedIndex], then, .value.

73 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 13
handleFormSubmit(event) {
... lines 15 - 17
const quantityInput = this.quantityInput.current;
const itemSelect = this.itemSelect.current;
... lines 20 - 21
... lines 24 - 25
... lines 27 - 73

This finds which option is selected, then returns its value attribute. Try it: refresh, select "Big Fat Cat", enter 50 and... boom! People, this is huge! We can finally pass real information to the callback. Uncomment onNewItemSubmit. Pass the options code, but, change to .text: this is the display value of the option. And, until we actually starting saving things via AJAX, that is what we'll pass to the callback. Next, use quantityInput.value.

72 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 13
handleFormSubmit(event) {
... lines 15 - 20
... lines 26 - 72

Updating the repLogs State

Finally, go back to RepLogApp and find handleNewItemSubmit: this is the function we just called. I'm going to change the argument to itemLabel, then clear things out. Ok, our job here is simple: add the new rep log to the repLogs state. Read the existing state with const repLogs = this.state.repLogs. Then, create the newRep set to an object. This needs the same properties as the other rep logs. So, add id set to... hmm. We don't have an id yet! Set this to "TODO". Then, reps: reps, itemLabel: itemLabel and, the last field is totalWeightLifted. Hmm, this is another tricky one! Our React app doesn't know how "heavy" each item is... and so we don't know the totalWeightLifted! Later, we'll need to ask the server for this info. For now, just use a random number.

51 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 24
handleNewItemSubmit(itemLabel, reps) {
const repLogs = this.state.repLogs;
const newRep = {
id: 'TODO-id',
reps: reps,
itemLabel: itemLabel,
totalWeightLifted: Math.floor(Math.random() * 50)
... lines 33 - 34
... lines 36 - 51

And finally, let's update the state! repLogs.push(newRep) and this.setState() with repLogs set to repLogs.

51 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 24
handleNewItemSubmit(itemLabel, reps) {
... lines 26 - 32
this.setState({repLogs: repLogs});
... lines 36 - 51

Um... there is a teeny problem with how we're updating the state here. But, we'll talk about it next. For now, gleefully forget I said anything was wrong and refresh! Fill out the form and... boo! A familiar error:

Cannot read property state of undefined in RepLogApp line 26

I've been lazy. Each time we create a handler function in a class, we need to bind it to this! In the constructor, add this.handleNewItemSubmit = the same thing .bind(this).

52 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 5
constructor(props) {
... lines 7 - 18
this.handleNewItemSubmit = this.handleNewItemSubmit.bind(this);
... lines 21 - 52

Using uuids

Try it again! We got it! It updates the state and that causes React to re-render and add the row. But... if we try it a second time, it does update the state, but, ah! It yells at us:

Encountered two children with the same key

Ah! The id property is eventually used in RepLogList as the key prop. And with the hardcoded TODO, it's not unique. Time to fix that temporary hack.

But, hmmm. How can we get a unique id? There are always two options. First, you can make an AJAX request and wait for the server to send back the new id before updating the state. We'll do that later. Or, you can generate a uuid in JavaScript. Let's do that now. And later, when we start talking to the server via AJAX, we'll discuss how UUIDs can still be used, and are a great idea!

To generate a UUID, find your terminal and install a library:

yarn add uuid --dev

Wait for that to finish... then go to RepLogApp and import uuid from uuid/v4. There are a few versions of UUID that behave slightly differently. It turns out, we want v4.

Down in constructor(), use UUID's everywhere, even in our dummy data. Then, move to the handle function and use it there.

53 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 3
import uuid from 'uuid/v4';
... lines 5 - 6
constructor(props) {
... lines 8 - 9
this.state = {
... line 11
repLogs: [
{ id: uuid(), reps: 25, itemLabel: 'My Laptop', totalWeightLifted: 112.5 },
{ id: uuid(), reps: 10, itemLabel: 'Big Fat Cat', totalWeightLifted: 180 },
{ id: uuid(), reps: 4, itemLabel: 'Big Fat Cat', totalWeightLifted: 72 }
... lines 18 - 20
... lines 22 - 26
handleNewItemSubmit(itemLabel, reps) {
... line 28
const newRep = {
id: uuid(),
... lines 31 - 33
... lines 35 - 36
... lines 38 - 53

Let's see if this fixes things! Move over, make sure the page is refreshed and start adding data. Cool: we can add as many as we want.

Clearing the Form

Which... is actually kinda weird: when the form submits, we need the fields to reset. No problem: in RepLogCreator, in addition to reading the values off of the DOM elements, we can also set them. At the bottom, use quantityInput.value = '' and itemSelect.selectedIndex = 0.

75 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 13
handleFormSubmit(event) {
... lines 15 - 25
quantityInput.value = '';
itemSelect.selectedIndex = 0;
... lines 29 - 75

Try it! Refresh... fill in the form and... sweet! Whenever you need to work directly with DOM elements, refs are your friend.

Leave a comment!