Happy Monday, everyone! Here I am again, trying to learn React the hard way, while showing you all my little mistakes. Let’s see what happened until now. (Or you can review it yourself, starting here)
What’s the state of the Application
We’ve already created a Log-in form, which pops up when the user clicks a button, has two input fields, for username and password, and prints the user input when the “Log In” button is pressed. No self-respecting web-site on the Internet would do that, but our does. For now.
What are we going to do today?
Today we’re going to make a way to close this form without printing out a user’s credentials. They are still going to show if someone tries to actually log-in, but if they decide to stop half-way through, they would be able. This would be done by creating a close button. This is hardly anything new, since we already created the “Log In” button and set up a click handler for it. That’s why I also plan on styling the form in such a way, that the input fields and the “Log In” button are vertical, as well as placing the close button up in the right corner of the form (if I have time). For this I would mainly need a bit of HTML in the LogIn component, but also some custom styles, to position the close button perfectly.
Now, let’s start with the HTML.
Restructuring the Login Form
First, I’m going to add another wrapping div around the form and add class names to it, the inner div and the inputs:
<div className="LoginPopup"> <div className="LoginForm"> <input className="LoginInput" type="text" name="username" id="username" placeholder="Username" onChange={this.change.bind(this)} /> <input className="LoginInput" type="password" name="password" id="password" placeholder="Password" onChange={this.change.bind(this)} /> <button className="LoginButton" type="submit" name="login" onClick={this.onSubmit.bind(this)}> Log In </button> </div> </div>
And now I’m going to add a button above the LoginForm div, like this:
<button className="ClosePopup" type="close" name="CloseLogin" onClick={this.onClose.bind(this)} > <img alt="closeButton" href="#"/> </button>
Good! We have our button! Only, the click handler is missing. I’ll add a function in our class right before the render method:
... } onClose() { alert("Bazinga"); } render() { ...
Great! Now our button works perfectly! It’s pretty ugly and it doesn’t close the popup though. Let’s close the popup first.
Closing the Popup
To close the popup we need to stop rendering it I guess. But what does that mean? How do we render stuff anyways? I think now is the first time I need to go and look up stuff on the Internet, before trying out anything. Let me go to my Jeeves replacement…
Right! Here we go! Apparently, if you change the state of a control, it re-renders automatically. I think that what this means is, we need to change the state of the Login Form, and also make it not render when the state says so. I’ll start by changing the state of the control:
... } onClose() { this.state["isOpen"] = false; } render() { ...
Great! Let’s see what happened. The entire page is broken, saying that it cannot read property of null. Alright, I’ll fix that! Let’s initialize the state property with an object in the constructor:
constructor(props) { super(props); this.state = {}; }
Now we have the page running. Let’s click the button. Nothing happens. All is good. Let’s use this state now. In the render function of the Login form, we’ll check if the state allows us to render the form. Like that:
render() { if (this.state["isOpen"]) { // render form } else { return null; } }
And now the result is, that the form doesn’t open at all. Progress! Let’s see what the problem is. We set the state, then we click the open button and it renders a LoginForm control. That means we are in the render method of the LoginForm control. Here we check what the state variable “isOpen” is. If it is true, we render. All good. Then we click the close button and set the “isOpen” state to false. Wait, where do we set it to true? This is never going to work. Let’s set the “isOpen” state to true in the constructor. I’ll just add:
this.state["isOpen"] = true;
Good! Now the form opens on click of the “Log In” button, and has the close button. Unfortunately, when I click the close button, nothing happens. Also, after clicking the close button and then clicking the “Log In” button, the form disappears, never to be seen again. Interesting… This means we need to fix two bugs now.
Perhaps if I make the LoginForm take its state from the App, before being rendered, I can make it show again. Let’s set a “prop” to the LoginForm when we create it. In the “openLoginForm” event handler of the App, I will replace the LoginForm JSX with this:
<LoginForm isOpen="true" />
Then in the constructor of the LoginForm, I’ll set the state entry “isOpen” to be the boolean value of the prop with the same name:
this.state["isOpen"] = Boolean(props.isOpen)
Well, this didn’t really help… I’ll debug it.
There it is! The constructor is not executed every time the application re-renders, only the render function. And this prevents the state from being reset. I need to find a way to update the state from the properties on each re-rendering. Hm…
Perhaps it not a good idea to let the LoginForm decide if it will be rendered, or entirely omitted. I think I can do something else. I will try to pass a callback function into the props of the LoginForm. This callback would be triggered on clicking the close button and it would change the state of the application. This way, the application would re-render and remove the LoginForm. Let’s try this.
Creating the onClose callback in the App
First, we can move the onClose event-handler from the LoginForm to the App component. We’re not going to use it anymore, so why not? Then I’ll rename it to “onLoginFormClose” Next, pass the function as prop to the LoginForm:
<LoginForm onClose={onLoginFormClose.bind(this)} />
You see? I didn’t forget to bind the handler to the App component this time! Of course, now we need to create the state of the App component in a constructor, so the application doesn’t fail with silly errors. This constructor would look very much like the LoginForm constructor:
constructor (props) { super(props); this.state = {}; this.state["isOpen"] = false; }
Further, the openLoginForm function now has to set the state of “isOpen” to true. Now, the hard part starts. The condidional rendering, which I introduced to the render method of the LoginForm actually needs to be in App. But there are other things which need to change in App still. Up until now, we are rendering the LoginForm through the openLoginForm click handler. If we need to rely on the re-rendering of the App component, this would break really fast. That’s why I’ll move all the rendering of the App control into its render method. Which still doesn’t exist. The click handlers would be left only with state changing logic. Hopefully it works. All this would make the App component look like that:
class App extends React.Component { constructor (props) { super(props); this.state = {}; } onLoginFormClose() { this.state["isOpen"] = false; } openLoginForm () { console.log("bazinga!"); this.state["isOpen"] = true; }; render () { <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <button className="App-link" onClick={openLoginForm} // href="#" > Log In </button> <div id="logIn"> if (this.state["isOpen"]) { <LoginForm onClose={onLoginFormClose.bind(this)}/> } </div> </header> </div> } }
Did you notice this if statement in the lower part of the snippet? The one that checks the state of the application and in some case, it renders the login form? I don’t know if this works. I’ll try it out.
Hm. Instead of executing, this if statement is printed out on the screen. Not only that, but it isn’t even there entirely. A chunk of it is missing. I guess this is too progressive for React :). No worries, I’ll try to extract the if statement on the top of the render function and keep the result of its operations in a variable. Then just reference it on the right place. This would look like:
let loginForm; if (this.state["isOpen"]) { loginForm = <LoginForm onClose={this.onLoginFormClose.bind(this)}/>; }
And then in the div with id “logIn”, I’ll use the variable:
<div id="logIn"> {loginForm} </div>
Oh, I forgot to set the close click handler properly in the LoginForm! That’s easy, just get the prop and set it on the button onClick attribute.
<button className="ClosePopup" type="close" name="CloseLogin" onClick={this.props.onClose} >
But the close button still doesn’t trigger the re-rendering. You know what, I’ve been ignoring a warning in the console for too long. It says I should not mutate state directly. I assume this means lines like ‘this.state[“isOpen”] = true’ should be avoided. I’ve seen somewhere another approach, one that uses a setter. I’ll try that:
onLoginFormClose() { this.setState({isOpen:false}); }
It Works!
Styling the LoginForm
Alright, now that we have a working “close” button, it is time to put some styles on this form. Perhaps something simple. I already added some class names, I assume the only thing we need to do right now is just to define the classes somewhere. I’ve heard of this nice new approach, where the css classes of your entire app are with local scope, but I think this is going to be complex enough for a blogpost on its own. Therefore I’m only going to create a new file, put the classes there, reference the file and use them. I will set up a background color for the Login popup to be a different color, the width of the div and the text-alignment:
.LoginPopup { background-color: #abd6ca; width: 50%; text-align: center; }
And I’ll save this class in a file called LoginForm.css. Of course this is not enough, because the application doesn’t know where this class is defined. I’ll follow the way App.css was referenced: I’ll import it:
import './LoginForm.css';
And Bob’s your Uncle!
What did we learn?
The most important thing I’ve learned today is, that state is hard to manage. The amount of work I did to actually make the form close was large enough to scare me away from the bad approach I had in the previous articles. I’m sure, the new approach I found is also going to evolve as I learn new things about React.
Another nice technique, which I learned was the conditional rendering. To be honest without it the close button functionality would be very clumsy.
An interesting thing is also, that I learned about lifting state, even though I didn’t call it like that. The thing is, the conditional rendering we applied here was set in the App component, which is the parent of the LoginForm. That’s why our state needed to be available in the App component and that’s why we moved the “isOpen” state property there. Apparently this is very important technique and even a good practice. I’ll be happy to learn more about it in the future.
Lastly, we’ve styled the login form. There wasn’t much to learn there yet, but I’m sure I can improve.
Next week: Using modern CSS techniques.
Until then, happy coding!
Are you still here?
Is this the first article of the series you read? Did you like it? If you want to read the whole series, here’s the start: Entering the 21-st century: Learning React
If you already know all the mistakes I made up until now, and you’re from the future, you can keep on laughing at me here: Modern CSS Techniques | Entering the 21-st Century by Learning React
I also have some other articles, feel free to browse around.