React Router and Creating a User Space

8 Dec

Welcome back to the series of articles about learning React. Start here, if you’re new. This time I’ll be playing around with React Router. Until now we have created a Login form, which is in a popup, and shows us the username and password, when they’re entered. Now I’m going to try to make a “Welcome” page for our users, who have successfully logged in.

I have thought a bit about how to do this, and easily came up with not so sane way of toggling the App markup, using a property in the state. Let’s look at it.

Not so sane routing

Last time we added a property in the state named isLoggedIn. This means, that when we are rendering, we can check for it and perhaps render something else there. Perhaps we’d wipe out the entire application, and render a new one.

Refactoring the home page

Right now, we’re checking for the isLoggedIn property, but we let everything else render, regardless. Let’s add all the other content into the else section of it:

  render () {
    let credentials;
    if (this.state["isLoggedIn"]) {
      credentials = <div>
        <span>username:{this.state.username}</span>
        <span>password:{this.state.password}</span>
      </div>;
    } else {
      let loginForm;
      if (this.state["isOpen"]) {
        loginForm = <LoginForm onClose={this.onLoginFormClose.bind(this)}
                               onSubmit={this.submitLoginForm.bind(this)}/>;
      }

      return <div className="App">
        <header className="App-header">
          <p>
            Welcome, Dude!
          </p>
          <button
            className="App-link"
            onClick={this.openLoginForm.bind(this)}
          >
            Log In
          </button>
          <div id="logIn">
            {loginForm}
            {credentials}
          </div>
        </header>
      </div>
    }
  }

There! Now, perhaps it’s a good idea to get rid of the rendering of the credentials. Well, when I remove everything in the if branch, all will be good. However, it is a good idea to also get rid of all the references of the credentials variable. Just to clean up the code a bit.

Ok, I got rid of the useless code already. And when I click the Login button this is what happens:

Empty web page is rendered in the browser after successfully routing away from the home page.
Web page contents after login

Yep! The page is empty. Exactly as it should be. That is actually where we’ll put all the components of the “Welcome back” page.

Creating a “Welcome back” page

Now, since there is a completely blank page on the screen right after a user logs in, the application is essentially dead. No way to go back, nothing to do on the page, just dead.

I say we put at least a warm greeting and a log-out button, so we have a way back. To do this, as we already know, we need to put stuff in the if branch of the App‘s render method. Like this:

  return <div>
      <p> Welcome, {this.state["username"]}!</p>
      <button
        className="App-link"
        onClick={this.logOut.bind(this)}
      >
        Log Out
      </button>
    </div>

This of course would fail, because I still haven’t written the click handler. You can see I’ve chosen the name logOut – boring, yes, but obvious. It’s important to choose obvious names for your methods. Anyhow, this method needs to change the state of the application in such a way, that the homepage gets rendered again. Also, it’s probably a really good idea if it wipes out the credentials too. This makes it look like that:

logOut () {
  const appState = {
    isOpen: false,
    isLoggedIn: false
  };

  this.setState(appState);
};

Modifying the state like this actually does not remove the credential properties. It just wipes out their values. But that’s fine. Now we have a button! And we are no longer stuck in the second page! Perfect, we’re done!

Why “if” statements are not sane for routing?

Well, it isn’t really too hard to see how once you get more than two pages, the code would branch out very fast. This would lead to large render function of the App component, to such an extent, that it won’t be really testable. And if this is not scary for you, I guarantee, that just the size of the file where this component is, is going to become very scary very fast.

Further, the more the properties in the state are, the harder it becomes to maintain each and every if statement: this page needs to render when property A is on, but not when property B is off. But it needs to watch out for property C, because then another page needs to render. Oh dear God, I’m starting to have anxiety already!

And if you decide to restructure the state, this whole thing is going to blow up, leaving you cleaning up afterwards for weeks. And I bet you wouldn’t really like it, would you?

React Router – my sanity conservation tool

Well, if we don’t need the craziness from the previous section, I better search for something that would prevent us from going there.

The search for a solution

Now, when I was looking for a way to do this, I found two different libraries offering simple solutions: React Router and React Navigation. It isn’t too difficult to find those two, actually. Google is helpful. YouTube is also helpful.

Now, React Navigation is pretty OK. I got the gist of it from a 5 minute video. But what I really liked with React Router, is that it works like React itself. Let me show you!

React Router in 5 minutes or less

First, of course I need to install it. It’s not too hard, just the standard npm command:

npm install --save-prod react-router-dom

The library provides us with some React components, which we can use to structure our pages, the way the site is going to be organized. Isn’t that nice? So instead of having a bunch of components scattered here and there, and then just define the paths to them, you organize them, the way we’ve built the Login form! Look:

<BrowserRouter>
    <Route path="/">
        <HomePage /> 
    </Route>
  <Route path="/welcome">
        <WelcomePage />
  </Route>
</BrowserRouter>

There! This is how you make two pages for two different paths. Now, there’s a little problem with the root path, but I’ll fix it when it actually needs to be fixed. Which is always later.

So, why does this conserve sanity? Because, you can actually use the Route component anywhere inside of the BrowserRouter body. Even if it is inside of another component! This means you only need to define the first level of routing in the main component, and then just deffer the responsibility to its children! Cool, right?

This, however, would mean I need to split all the App code into separate components. This might help with my biggest fear – state management. We’ll see.

Preparing the application for React Router

Up until now, all the code is in one file. That doesn’t scare me too much currently, but the fact that the two different pages are both defined in the App control is what really gets me. Luckily, React Router requires a component for each route. This means now is the best time to fix the application structure. I’ll create a component for each route and put in it the markup from each branch in the router function of the App component.

Create a Home Page component

When the user just lands on our website, the render method of the App component returns this:

      let loginForm;
      if (this.state["isOpen"]) {
        loginForm = <LoginForm onClose={this.onLoginFormClose.bind(this)}
                               onSubmit={this.submitLoginForm.bind(this)}/>;
      }

      return <div className="App">
        <header className="App-header">
          <p>
            Welcome, Dude!
          </p>
          <button
            className="App-link"
            onClick={this.openLoginForm.bind(this)}
          >
            Log In
          </button>
          <div id="logIn">
            {loginForm}
          </div>
        </header>
      </div>

This snippet in turn renders the LoginForm. Now, I’ll move this snippet to a separate class, named HomePage, and just return <HomePage />. Like in the article, where I play around with CSS (Entering the 21-st Century by Learning React: Closing the Popup), I need to pass some callbacks as properties, to be able to update the state and thus communicate with the other pages. In this case, I need to pass the submitLoginForm handler. I think opening and closing the popup would be local matter for the new component. Let’s try this.

class HomePage extends React.Component {
  constructor (props) {
    super(props);
    this.state = {};
  }

  onLoginFormClose() {
    this.setState({isLoginFormOpen:false});
  }

  openLoginForm () {
    console.log("bazinga!");
    const homePageState = {
      isLoginFormOpen: true
    };
    this.setState(homePageState);
  };

  render () {
    let loginForm;
    if (this.state["isLoginFormOpen"]) {
      loginForm = <LoginForm onClose={this.onLoginFormClose.bind(this)}
                             onSubmit={this.props.submitLoginForm}/>;
    }

    return <div className="App">
      <header className="App-header">
        <p>
          Welcome, Dude!
        </p>
        <button
          className="App-link"
          onClick={this.openLoginForm.bind(this)}
        >
          Log In
        </button>
        <div id="logIn">
          {loginForm}
        </div>
      </header>
    </div>
  };
}

This looks fine, I even tested it by rendering it in the App component. Everything works, let’s go on with the other component.

Create the Welcome Page

The same procedure needs to be done for the other branch of App’s render function. It needs the username of the logged in user and also a way to log out, i.e. a handler that changes the App state. I’ll be passing these as properties:

class WelcomePage extends React.Component {
  constructor (props) {
    super(props);
    this.state = {};
  }
  render () {
    return <div>
        <p> Welcome, {this.props["username"]}!</p>
        <button
          className="App-link"
          onClick={this.props.logOut}
        >
          Log Out
        </button>
      </div>
  }
}

This one is much better – just a few lines. Anyhow, let’s finally do something new today. If we’re lucky, I’ll fail several times. I’m putting it all together.

Finally routing between pages

This step would make me clean up the App component already. I’ll get rid of all the unnecessary click-handlers and excessive state. Then I’ll just setup the routes to the different pages.

Since we need to manage state on the highest component level, I need to leave the submitLoginForm and the logOut handlers. The rest would go. Now the class looks much simpler:

class App extends React.Component {
  constructor (props) {
    super(props);
    this.state = {};
  }
  submitLoginForm (username, password) {
    const appState = {
      username: username,
      password: password,
      isLoggedIn: true
    };

    this.setState(appState);
  };
  logOut () {
    const appState = {
      isOpen: false,
      isLoggedIn: false
    };

    this.setState(appState);
  };
  render () {
    if (this.state["isLoggedIn"]) {
      return <WelcomePage username={this.state.username} logOut={this.logOut.bind(this)} />
    } else {
      return <HomePage submitLoginForm={this.submitLoginForm.bind(this)} />
    }
  }
}

Of course, it is still using if statements for routing, which is just what I want to remove. That’s why I’ll import a few components from the React Router for the browser:

import {
  BrowserRouter as Router,
  Route
} from "react-router-dom";

And then I’ll put the page components in as routes in a switch:

  render () {
    return <Router>
      <Route path="/">
        <HomePage submitLoginForm={this.submitLoginForm.bind(this)} />
      </Route>
      <Route path="/welcome">
        <WelcomePage username={this.state.username} logOut={this.logOut.bind(this)} />
      </Route> 
    </Router>
  }

If I try the app now, I get a working LoginForm! Yay! But when I try to actually enter credentials and authenticate, nothing happens!

React Router doesn’t care about state

I just realized, that there is no place whatsoever in my code, which alters the URL. Everything happens in a single resource! All the pages are just some state. While this was working OK until now, it is genuinely ugly and I don’t like it. Also React Router doesn’t want to render anything else but the home page. It doesn’t know and doesn’t care about any changes in the state.

This is why I actually need to find a way to change the URL, once the user clicks on the Log In button. I’ll look around in the internet.

Well, the GitHub page for react-router has some neat docs. Here’s the page for a Redirect component. If I use it as they show in their first example I get a pretty messy, but working render function in my App component:

  render () {
    return <Router>
      <Route path="/">
        { this.state.isLoggedIn ?
          <Redirect to="/welcome" /> :
          <HomePage submitLoginForm={this.submitLoginForm.bind(this)} />
        }
      </Route>
      <Route path="/welcome">
        { this.state.isLoggedIn ?
          <WelcomePage username={this.state.username} logOut={this.logOut.bind(this)} /> :
          <Redirect to="/" />
        }
      </Route> 
    </Router>
  }

I don’t really like it though. So I’ll do something else. I’ll move the redirection to each component, and just pass the isLoggedIn property. Then I’ll render the Redirect before anything else in the components, if I need to. This would be the App’s render method:

  render () {
    return <Router>
      <Route path="/">
        { this.state.isLoggedIn ?
          <Redirect to="/welcome" /> :
          <HomePage submitLoginForm={this.submitLoginForm.bind(this)} />
        }
      </Route>
      <Route path="/welcome">
        { this.state.isLoggedIn ?
          <WelcomePage username={this.state.username} logOut={this.logOut.bind(this)} /> :
          <Redirect to="/" />
        }
      </Route> 
    </Router>
  }

And then I’ll have a check in the components similar to that:

    if (!this.props.isLoggedIn) {
      return <Redirect to="/" />
    }

Of course, if the isLoggedIn property is true, the other component would redirect. There’s only a slight problem now. Only the HomePage renders. If I try to log into the application, I get redirected back to HomePage. Even if I write the WelcomePage url, I still get back to the HomePage.

Fixing that slight problem of route matching

All over the Internet one can see warnings about writing stuff like:

<Route path="/">

And, apparently, I should have listened. I’m sure when I add the attribute “exact” right before “path”, everything would be fine. Like this:

<Route exact path="/">

We have our User Space!

Finally, we now have a page, which our users can enjoy, after they provide credentials! For now it is a very empty one:

A dashboard page, which renders after navigating with Router React.
Working Welcome Page

Nonetheless, it has a button, which would bring us back to the Home Page and let us log in again. This way we can actually keep logging in and logging out until it’s dark outside! Perfect!

And look at the URL! It says “welcome”! Nice!

I’ve put the code into a repository!

Hey, look, I know this is way too complex already. If you want to see the results from each article from now on, I have put a complete version of the application in a Bitbucket repository. Each article would have its own branch, named after it. Here’s the one for this article.

What Are We Doing Next?

Since we already have a pretty functional front-end, albeit really ugly one, I think I want to play around a bit with a back-end technology. That’s why I think next time, I’ll set up a Flask application, that communicates with our front-end and “checks” if our credentials are valid or not. If you’re not interested in that, I’m sure you’ll like an article I’ve planned about Redux. I’ll link it <here>, when it’s done.

But for now, you can have a look at this funny article I did a while back. It’s about a table.

Or perhaps this article: Entering the 21-st Century by Learning React: Closing the Popup – here I learn how to apply the state lifting technique.

Happy coding!