The exercises in this seventh part of the course differ a bit from the ones before. In this chapter there is, as usual, three exercises related to the theory in this chapter.

In addition to the exercises in this chapter, there is a series of exercises which revise what we've learned, and where the theory in this part is applied. In this part, we expand the Bloglist application which we worked on during parts 4 and 5.

Application navigation structure

Following part 6, we return to React without Redux.

It is very common for web-applications to have a navigation bar, which enables switching the view of the application.

fullstack content

and separate pages for showing information on notes and users:

fullstack content

In an old school web app, changing the page shown by the application would be accomplished by the browser making a HTTP GET request to the server and rendering the HTML representing the view that was returned.

In single page apps, we are, in reality, always on the same page. The Javascript code run by the browser creates an illusion of different "pages". If HTTP requests are made when switching view, they are only for fetching JSON formatted data, which the new view might require for it to be shown.

The navigation bar and an application containing multiple views is very easy to implement using React.

Here is one way:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const Home = () => (
  <div> <h2>TKTL notes app</h2> </div>
)

const Notes = () => (
  <div> <h2>Notes</h2> </div>
)

const Users = () => (
  <div> <h2>Users</h2> </div>
)

const App = () => {
  const [page, setPage] = useState('home')

 const toPage = (page) => (event) => {
    event.preventDefault()
    setPage(page)
  }

  const content = () => {
    if (page === 'home') {
      return <Home />
    } else if (page === 'notes') {
      return <Notes />
    } else if (page === 'users') {
      return <Users />
    }
  }

  const padding = {
    padding: 5
  }

  return (
    <div>
      <div>
        <a href="" onClick={toPage('home')} style={padding}>
          home
        </a>
        <a href="" onClick={toPage('notes')} style={padding}>
          notes
        </a>
        <a href="" onClick={toPage('users')} style={padding}>
          users
        </a>
      </div>

      {content()}
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

Each view is implemented as its own component. We store the view component information in the application state called page. This information tells us which component, representing a view, should be shown below the menu bar.

However, the method is not very optimal. As we can see from the pictures, the address stays the same even though at times we are in different views. Each view should preferably have its own address, e.g. to make bookmarking possible. The back-button doesn't work as expected for our application either, meaning that back doesn't move you to the previously displayed view of the application, but somewhere completely different. If the application were to grow even bigger and we wanted to, for example, add separate views for each user and note, then this self made routing, which means the navigation management of the application, would get overly complicated.

Let's change the above application to use React router. First, we install React router with the command

npm install --save react-router-dom

The routing provided by React Router is enabled by changing the application as follows:

import {
  BrowserRouter as Router,
  Route, Link, Redirect, withRouter
} from 'react-router-dom'

const App = () => {

  const padding = { padding: 5 }

  return (
    <div>
      <Router>
        <div>
          <div>
            <Link style={padding} to="/">home</Link>
            <Link style={padding} to="/notes">notes</Link>
            <Link style={padding} to="/users">users</Link>
          </div>
          <Route exact path="/" render={() => <Home />} />
          <Route path="/notes" render={() => <Notes />} />
          <Route path="/users" render={() => <Users />} />
        </div>
      </Router>
    </div>
  )
}

Routing, or the conditional rendering of components based on the url in the browser, is used by placing components as children of the Router component, meaning inside Router-tags.

Notice that, even though the component is referred to by the name Router, we are in fact talking about BrowserRouter, because here the import happens by renaming the imported object:

import {
  BrowserRouter as Router,
  Route, Link, Redirect, withRouter
} from 'react-router-dom'

According to the manual

BrowserRouter is a Router that uses the HTML5 history API (pushState, replaceState and the popState event) to keep your UI in sync with the URL.

Normally the browser loads a new page when the URL in the address bar changes. However, with the help of the HTML5 history API BrowserRouter enables us to use the URL in the address bar of the browser for internal "routing" in a React-application. So, even if the URL in the address bar changes, the content of the page is only manipulated using Javascript, and the browser will not load new content form the server. Using the back and forward actions, as well as making bookmarks, is still logical like on a traditional web page.

Inside the router we define links that modify the address bar with the help of the Link component. For example,

<Link to="/notes">notes</Link>

creates a link in the application with the text notes, which when clicked changes the URL in the address bar to /notes.

Components rendered based on the URL of the browser are defined with the help of the component Route. For example,

<Route path="/notes" render={() => <Notes />} />

defines that if the address in the browser is /notes, then the component Notes is rendered.

The root of the application, or the address /, is defined to render the component Home:

<Route exact path="/" render={() => <Home />} />

We have to use the modifier exact in front of the path attribute of the route. Otherwise Home is also rendered on all other paths, since the root / is included at the start of all other paths.

Parameterized route

Let's examine the slightly modified version from the previous example. The complete code for the example can be found here.

The application now contains five different views, the display of which is controlled by the router. In addition to the components from the previous example (Home, Notes and Users), we have Login representing the login view and Note representing the view of a single note.

Home and Users are unchanged from the previous exercise. Notes is a bit more complicated. It renders the list of notes passed to it as props in such a way that the name of each note is clickable.

fullstack content

The ability to click a name is implemented with the component Link, and clicking the name of a note whose id is 3 would trigger an event that changes the address of the browser into notes/3:

const Notes = (props) => (
  <div>
    <h2>Notes</h2>
    <ul>
      {props.notes.map(note =>
        <li key={note.id}>
          <Link to={`/notes/${note.id}`}>{note.content}</Link>
        </li>
      )}
    </ul>
  </div>
)

When the browser transitions to the address singling out the specific note, such as notes/3, the component Note is rendered:

const Note = ({ note }) => (
  <div>
    <h2>{note.content}</h2>
    <div>{note.user}</div>
    <div><strong>{note.important ? 'important' : ''}</strong></div>
  </div>
)

This happens by expanding the routing in the App component as follows:

<Router>
  <div>
    <div>
      <Link style={padding} to="/">home</Link>
      <Link style={padding} to="/notes">notes</Link>
      <Link style={padding} to="/users">users</Link>
    </div>

    <Route exact path="/" render={() =>
      <Home />
    } />
    <Route exact path="/notes" render={() =>      <Notes notes={notes} />    } />    <Route exact path="/notes/:id" render={({ match }) =>      <Note note={noteById(match.params.id)} />    } />  </div></Router>

A modifier exact path="/notes" has been added to the route rendering all notes, because otherwise it would also be rendered for all paths that are in the form /notes/3.

The route rendering a single note is defined "in the style of express" by specifying the parameter of the route with the notation :id

<Route exact path="/notes/:id" />

The render attribute, which defines the component to be rendered, can access the id using its parameter called match in the following way:

render={({ match }) =>
  <Note note={noteById(match.params.id)} />}

The note corresponding to the id in match.params.id is resolved using a helper function noteById

const noteById = (id) =>
  notes.find(note => note.id === Number(id))

and finally the Note-component being rendered gets the note singled out by the unique part of the URL as one of its props.

withRouter and history

We have also implemented a very simple login feature. If a user is logged into the application, we store the information about this user in the App's state user

The option to navigate to the Login-view is rendered conditionally in the menu.

<Router>
  <div>
    <div>
      <Link style={padding} to="/">home</Link>
      <Link style={padding} to="/notes">notes</Link>
      <Link style={padding} to="/users">users</Link>
      {user        ? <em>{user} logged in</em>        : <Link to="/login">login</Link>      }    </div>
  </div>
</Router>

So if the user is already logged in, instead of displaying the link Login we show the username of the user:

fullstack content

The code of the component handling the login functionality is as follows

import {
  // ...
  withRouter} from 'react-router-dom'

const LoginNoHistory = (props) => {
  const onSubmit = (event) => {
    event.preventDefault()
    props.onLogin('mluukkai')
    props.history.push('/')  }

  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          username: <input />
        </div>
        <div>
          password: <input type='password' />
        </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

const Login = withRouter(LoginNoHistory)

There are a few notable things about the implementation of the form. When logging in, we call the function onSubmit, which calls a method called push of the history-object received by the component as a prop. The command props.history.push('/') results in the address bar of the browser changing its address to / thereby making the application render the respective component, which in this case is Home.

The component gets access to the history-prop after it is "wrapped" by the function withRouter.

redirect

There is one more interesting detail about the Users route:

<Route path="/users" render={() =>
  user ? <Users /> : <Redirect to="/login" />
} />

If a user isn't logged in, the Users component is not rendered. Instead the user is redirected using the Redirect-component to the login view

<Redirect to="/login" />

In reality it would perhaps be better to not even show links in the navigation bar requiring login if the user is not logged into the application.

Here is the App component in its entirety:

const App = () => {
  const [notes, setNotes] = useState([
    {
      id: 1,
      content: 'HTML on helppoa',
      important: true,
      user: 'Matti Luukkainen'
    },
    // ...
  ])

  const [user, setUser] = useState(null)

  const login = (user) => {
    setUser(user)
  }

  const noteById = (id) =>
    notes.find(note => note.id === Number(id))

  const padding = { padding: 5 }

  return (
    <div>
      <Router>
        <div>
          <div>
            <Link style={padding} to="/">home</Link>
            <Link style={padding} to="/notes">notes</Link>
            <Link style={padding} to="/users">users</Link>
            {user
              ? <em>{user} logged in</em>
              : <Link to="/login">login</Link>
            }
          </div>

          <Route exact path="/" render={() =>
            <Home />
          } />
          <Route exact path="/notes" render={() =>
            <Notes notes={notes} />
          } />
          <Route exact path="/notes/:id" render={({ match }) =>
            <Note note={noteById(match.params.id)} />
          } />
          <Route path="/users" render={() =>
            user ? <Users /> : <Redirect to="/login" />
          } />
          <Route path="/login" render={() =>
            <Login onLogin={login} />
          } />
        </div>
      </Router>
      <div>
        <br />
        <em>Note app, Department of Computer Science 2019</em>
      </div>
    </div>
  )
}

We define an element common for modern web apps called footer, which defines the part at the bottom of the screen, outside of the Router, so that it is shown regardless of the component shown in the routed part of the application.