Interactivity Finally — Solution
December 03, 2019
This page shows the steps to derive the JavaScript for the “Interactivity - Finally!” application from the example app.
It shows just the JavaScript and JSX in the App.js
file. It doesn’t show the
CSS.
Level 1: Easy(er)
Study the example app that Christian demonstrated in the lab: https://repl.it/@ChristianGrewel/State-and-Interaction-1. In that application, the pair of variables
count
/setCount
tracks the number of times a button has been pressed:count
is that number of times, andsetCount
supplies a new value forcount
.
In your program, you want to keep track of whether a button has been pressed. How can you adapt the code in the example app code to your purpose?
First, let’s remove the code that cares about whether the button has been clicked 100 times.
Now nothing special will happen on the 100th click.
The example app counts the number of times that a button has been pressed. (This is the program’s state.)
This number of times is represented by a JavaScript number, that starts at 0 and is incremented each time the user presses (clicks) the button.
In our program, we want to keep track of whether the button has been pressed.
This is the answer to a yes/no question: Has the button been pressed? Yes! (Or, No!)
You can also think about this as an assessment of the statement “The button has
been pressed”. This statement is true if the button has been pressed, and false
if it hasn’t. (This is just like age >= 16
from earlier in the course. This is
an expression that is true if the value of age
is greater or equal to 16, and
false otherwise.)
true
and false
are JavaScript values, so let’s use them.
Instead of using the state variable to count, we’ll use it to keeping track of whether the button has been presssed.
count
is no longer a good name for this variable. Rename it to pressed
.
Use the ternary operator ? :
to compute a different class name depending on
whether the button has been pressed.
pressed ? "pressed button" : "button"
has the value "pressed button"
if
pressed
is true, and "button"
otherwise. It’s like if (pressed) { "pressed button" } else { "button" }
,
except it can be used as a value.
In JSX, as in HTML, an element with the class name “pressed button” has two CSS
classes: pressed
and button
. It therefore matches these CSS selectors:
.pressed { … }
.button { … }
.pressed.button { … }
.button.pressed { … }
Since this element is a <button>
, it also matches these CSS selectors:
button.pressed { … }
button.button { … }
button.pressed.button { … }
button.button.pressed { … }
(button.button
, that matches an HTML tag with name “button” and CSS class
“button”, looks redundant, but this is how a number of CSS frameworks do
styling.)
Apply the class name to the <button>
element.
Since buttonClass
is a JavaScript variable, it needs to go inside { }
to use
its value inside JSX.
Extract the code that constructs the button JSX, and handles its click, into a
new component PressableButton
.
Parameterize the button name, so that we can give different names to different PressableButton’s.
Recall that the JSX <PressableButton name="Click me" />
is equivalent to the
JavaScript PressableButton({ name: "Click me" })
— a JSX tag’s properties
become the JavaScript properties of a JavaScript object, that is the single
argument to the function that implements the conmponent.
Extracting the code for the pressable button into a component sets us up to add a second button.
Add the second button (and rename the first).
Part 1 – Finishing UP
At this point, we may note that the assignment doesn’t fully specify the required behavior. (This is typical for requirements.)
Should clicking on a button a second time toggle it — return it to its original (unpressed) state?
Should clicking on one button change the other button to its original state? (This would make these buttons act like HTML radio buttons.)
The current implementation answers both these questions as “no”.
Technically, this code therefore completes Level 1 of the assignment (or will once we add a third button):
- Each button needs to be clickable.
- Clicking on a button should highlight the button in some way
Toggle Buttons
In order to explore more JavaScript and React, we’ll go ahead and try out different answers to these questions about the requirements.
Note that we’re tackling these questions before adding the third button. It’s easier to try things out while there’s two buttons than once there’s three.
The exclamation point is the JavaScript not operator. It goes before a value, and has the meaning ”! true” is false, ”! false” is true.
In order for interacting with one PressableButton to affect the state of another, they need access to each other’s state variables.
The simplest way to do this is to move the state variabes to their container (ExampleInteraction), and have ExampleInteraction pass the variables and their setter functions to the PressableButton instances, as properties.
So far this doesn’t change the functionality of the program. (Pressing one button doesn’t actually unpress the other.) It’s simply setting up for that change…
This implements radio-button functionality.
Note: In some ways, this looks like a step backwards! Adding a PressableButton
component meant we didn’t have to duplicate code in order to add another button.
But now, adding a third button would involve making a copy of useState
line,
and making a copy of the handleClick
function, as well as adding a
<PressableButton … />
line.
The problem is because there’s a mismatch between the states of our program, and the values of our state variables.
Our program can be in one of these states:
- Neither button is pressed.
- “First Button” is pressed.
- “Second Button” is pressed.
Our state variables can express these states:
pressed1
is false,pressed2
is false. (Program state #1.)pressed1
is true,pressed2
is false. (Program state #2.)pressed1
is false,pressed2
is true. (Program state #3.)pressed1
is true,pressed2
is true. (Can’t happen.)handleClick1
andhandleClick2
have to set all the state variables, in order to keep out of the disallowed program state where both variables are clicked.
The mismatch will get worse when we add a third button. Then there will be only more program state:
- Neither button is pressed.
- “First Button” is pressed.
- “Second Button” is pressed.
- “Third Button” is pressed.
but four more state variable states.
A better design for this program would involve a single state variable, that indicates which button is pressed. It’s value could be a number (1, 2, or 3 for one of the buttons), or a name (“First Button”, “Second Button”, “Third Button”).
pressedButtonName
is the name of the currently-pressed button.
This code uses the special JavaScript value null
to represent “no button”.
This is generally better than values such as 0, false, or the empty string ""
to represent the absence of a value.
Having to type “First Button” twice (once in handleClick1
, and once as the
PressableButton name
property) makes for fragile code. If the name is changed,
it must be changed in both places.
We make this value a parameter value for handleClick
, and use the component
property name.
handleClick
just calls setPressedButtonName
with the same argument, so we
can eliminate handleClick
and use setPressedButtonName
instead.
Time to clean up some names.
Also, now that PressableButton
accesses props.name
three times, we’ll go
ahead an introduce a variable that holds this value.
This is mainly for brevity. In performance-critical code, which this is not, it can make a program faster.
You can make the code shorter by using more advanced JavaScript features:
destructuring bind, and the arrow syntax () => …
for anonymous functions.
We can even use destructuring bind to read the properties directly out of the
argument value, instead of giving it a name (props
) first.
Warning: Since buttons already have a name, this is a convenient way to write this. It introduces a point of fragility into the program, though: each button must have a unique name. Think about what could go wrong in a user interface that identified college students at a University, or Amazon products, by name!