Adding a Login form

Hrishi Mittal
Now that we have an authentication API endpoint, let’s add a way to use it from the frontend React app. Since we’ve already created a user account from the command line, let’s first add a login form component.

Create the component file Login.js in the components directory with a class component called Login, which will render a simple form with email and password fields and a submit button.

components/Login.js:

import React from 'react'

class Login extends React.Component {
  render () {
    return (
      <div>
        <h2>Log in</h2>
        <form onSubmit={this.handleLogin} >
          <input name="email" ref={(input) => this.email = input } />
          <input name="password" type="password" ref={(input) => this.password = input } />
          <input type="submit" value="Log in" />
        </form>
      </div>
    )
  }
}

export default Login

Note I’ve written this as an uncontrolled component accessing field values via refs since I don’t need to use state in this component. You can add an event handler if you prefer. I’ll leave that as an exercise for the end of this module.

We want to handle the form submission with a function called handleLogin, so let’s define that now. We’ll use axios to POST the form to our authentication endpoint for signing the user in:

components/Login.js:

import React from 'react'
import axios from 'axios'

class Login extends React.Component {
  handleLogin = (e) => {
    e.preventDefault()
    axios({
      method: 'POST',
      url: 'http://localhost:3001/auth/sign_in',
      data: {
        email: this.email.value,
        password: this.password.value
      }
    })
    .then(response => {
      console.log(response)
    })
  }

  render () {
    return (
      <div>
        <h2>Log in</h2>
        <form onSubmit={this.handleLogin} >
          <input name="email" ref={(input) => this.email = input } />
          <input name="password" type="password" ref={(input) => this.password = input } />
          <input type="submit" value="Log in" />
        </form>
      </div>
    )
  }
}

export default Login

For now, we’re just logging the response. Later, we’ll look at persisting the logged in user’s details.

Now, let’s use this form inside the App component.

import React from 'react'
import Login from './components/Login'
import Eventlite from './components/Eventlite'

function App() {
  return (
    <div className="App">
      <Login />
      <Eventlite />
    </div>
  )
}

export default App


Now if you enter the email and password of the test user we created on the command line and submit the form, you’ll see a successful response in the developer console. There are lots of details in the response, including a status 200 (indicating success), but the things we are interested in are the uid, client and access token. We’ll need these to make subsequent requests to the API later on.

If you look in the Rails development log, you’ll notice a warning:

Unpermitted parameter: :session
This happens because Rails is automatically wrapping the login form parameters inside a parameter called session:

  Parameters: {"email"=>"[email protected]", "password"=>"[FILTERED]", "session"=>{"email"=>"[email protected]", "password"=>"[FILTERED]"}}
We can fix this in config/initializers/wrap_parameters.rb by removing :json from the wrap_parameters format options array.

Change this:

ActiveSupport.on_load(:action_controller) do
  wrap_parameters format: [:json]
end

to this:

ActiveSupport.on_load(:action_controller) do
  wrap_parameters format: []
end


Persisting session data to localStorage

Now, if you restart the Rails server and submit the form again, the warning will disappear and you will see the form parameters submitted like this in the Rails log.

  Parameters: {"email"=>"[email protected]", "password"=>"[FILTERED]"}
Ok, so we are able to make a successful authentication request but we need to persist the logged in user to make use of the authentication.

Now, we could save the user’s details in the app state but then we’d lose them on page reload. We want to be able to persist a user’s session even if they close the app’s browser window and then open it again later. We’ll end their session only when they choose to log out.

We can use the browser’s localStorage property to store the session details. localStorage has methods for setting, getting and deleting item values as key value pairs.

Let’s save the session details in a localStroage item called ‘user’. We’ll construct a JSON string with the values we need from the successful authentication response.

So, in the handleLogin function in Login.js, add this:

    .then(response => {
      console.log(response)
      localStorage.setItem('user',
        JSON.stringify({
          'access-token': response.headers['access-token'],
          'client': response.headers['client'],
          'uid': response.data.data.uid
      }))
    })

Now that the user details will be persisted, we can use them in App.js. At the moment, we’re simply displaying the Login form above the rest of the app components.

In the next module, we will add separate routes for login, signup and the core app functionality. But for now, we can do a conditional render - if a user hasn’t already logged in, we will display only the login form. If a user has logged in, we will only display the Event components (no login form).

So inside App() in App.js, let’s write a conditional return based on whether a current user exists:

function App() {
  return (
    <div className="App">
      {currentUser() ? <Eventlite /> : <Login />}
    </div>
  )
}
Then let’s define currentUser as a function which reads the ‘user’ item from localStorage, which we defined earlier in Login.js:

const currentUser = function() {
  const user = localStorage.getItem('user')
  return(user)
}
So now, the full App.js file looks like this:

import React from 'react'
import Login from './components/Login'
import Eventlite from './components/Eventlite'

const currentUser = function() {
  const user = localStorage.getItem('user')
  console.log(user)
  return(user)
}

function App() {
  return (
    <div className="App">
      {currentUser() ? <Eventlite /> : <Login />}
    </div>
  )
}

export default App

Now, if you load the app in the browser, you will see just the Login form. If you submit the form with a valid email and password and then reload the page, the Login form will disappear and the rest of the app with the event form and list will appear.

We needed to manually reload the page after submitting the login form because we’re not using state and the App component is not able to pick up the change in localStorage. To avoid hte manual reload, let’s set the window location in handleLogin to the homepage after updating localStorage.

    .then(response => {
      console.log(response)
      localStorage.setItem('user',
        JSON.stringify({
          'access-token': response.headers['access-token'],
          'client': response.headers['client'],
          'uid': response.data.data.uid
      }))
      window.location = '/'
    })
Now if we clear the browser cache (or remove the user item manually by calling localStorage.removeItem(‘user’) from the developer console) and try again the page reload will happen automatically.

Directly setting the window location in this way doesn’t feel very clean but we’ll use it for now, because we are going to use React Router in the next module to handle all routing.

If you prefer using state, you could store the session details in state and localStorage, and only read them from localStorage into state on initial app load. But we’ll keep things simple for now, since we’ll change them shortly anyway.

Now, we’re logged in with a user account and still able to create events via EventForm. However, we haven’t linked events to users in the Rails backend, so any new events won’t belong to the user creating them.

In the next lesson, let’s link the User and Event models.