React/Rails Authentication: Client to API Communication

This is the final entry in a short series about building a basic authentication app using a React client and a Rails API. This post builds upon the previous posts on API Configuration and Client Initialization, so if you haven’t checked those out already, please do so in order to follow along.

In this post, I will be jumping back and forth between the API directory and the Client directory, so I’ll do my best to be transparent about where the code lives in the example.

The functionality we have built so far just simulates the log in process. In order to actually log someone in using Rails sessions, we need to add a couple of things to our controllers. I’ll start in the application controller:

class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
before_action :set_current_user
def set_current_user
if session[:user_id]
@current_user = User.find(session[:user_id]
end
end
end

There are a couple of things getting added:

First, we are skipping Rails’ default behavior of setting an authenticity token. If you’re not familiar with this, I won’t go into detail, but basically since we aren’t using Rails views and have our Client whitelisted in cors.rb, we need to tell Rails not to check for its own authenticity or else we will be blocked from our API’s resources.

Next, we define the method set_current_user that checks Rails’ session hash for a user_id. If it finds one, we then have access to a @current_user instance variable. Otherwise, that variable will be nil, a falsey value. We have a macro at the top called before_action. This Rails macro is pretty self explanatory, it simply says to run our set_current_user method before ANY controller action. Since our controllers all inherit from Application Controller, this method will run throughout all controller actions in our app now.

Open up controllers/sessions_controller.rb in your API folder and let’s add some actions:

class SessionsController < ApplicationController def create
user = User.find_by(username: params[:username])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
render json: {
logged_in: true,
user: @current_user
}
else
render json: {
logged_in: false,
error: 'Invalid username/password combination'
}
end
def destroy
session.clear
render json: { logged_in: false }
end
def logged_in
if @current_user
render json: { logged_in: true, user: @current_user }
else
render json: { logged_in: false }
end
end
end

Time to break down the changes:

In the create action, we added a line right above the json rendering. This line sets a key in the session hash equal to the found user’s id. After this line, our @current_user variable will now return a specific user record.

The destroy action is very straightforward: clear the session hash of any keys we set and render json verifying it.

The logged_in action is an endpoint that we will use to maintain state on the front end. Its sole purpose is to check if a user is logged in or not and render json, respectively.

The final piece that we need are routes that point to these actions:

Rails.application.routes.draw do
post "/login", to: "sessions#create"
get "/logged_in", to: "sessions#logged_in"
get "/logout", to: "sessions#destroy"
end

The API end of things looks good for now, so I’ll jump back to our Client app.

We left off with a simple Home component for our client, but there’s a few things I want to add and/or change.

Now, let’s make another component: Dashboard.js. The Dashboard is a functional component, it doesn’t need to maintain any kind of state on its own.

import React from 'react';const Dashboard = props => {
return(
<div>
<h1>Dashboard</h1>
<button type="submit" onClick={props.logout}>Log Out</button>
</div>
)
}
export default Dashboard;

At some point, our dashboard is going to accept the props from App to show our logged in user. For now, we just have the <h1> tag to make sure it renders. In those props will be the function that we use to log a user out, essentially resetting the session hash and returning to our default application state in the client.

Now let’s move to App.js. When generating a React app through create-react-app, it generates this component as a functional component, but we need a class component that maintains state for this specific case. Additionally, in order to redirect after a successful login, we will pull in BrowserRouter from react-router-dom . If you aren’t familiar with React Router, you can read more about it here.

Another dependency I want to add is axios. In the root of your client directory, run npm install axios. This is the library we will use to fetch to our API.

import React { Component } from "react";
import './App.css';
import Home from './Home.js';
import { BrowserRouter, Switch } from 'react-router-dom';
import axios from 'axios';
export default class App extends Component {
state = {
currentUser: undefined
}
render() {
return(
<div className="App">
<BrowserRouter>
<Switch>
<Route exact path={'/'}
render={props =>
<Home {...props}
handleLogin={this.handleLogin}
/>}
/>
<Route exact path={'/dashboard'}
render={props =>
<Dashboard {...props}
currentUser={this.state.currentUser}
/>}
/>
</Switch>
</BrowserRouter>
</div>
)
}
}

Now that we have the functionality to maintain state, we have a key in the state object of currentUser that points to undefined. Essentially, the idea here is that once we finish communicating with our API and storing the session cookie, we then update this value to the actual user instance returned from the API. From there, we would be able to call attributes on the user instance such as currentUser.username and so forth.

If you’ve noticed, App is our parent component, and Home and Dashboard are both acting as child components. Because of this, we need to update App's state through a function that we will pass to Home, called HandleLogin. Above, you’ll see it’s already passed as a prop to our Home component.

handleLogin = data => { 
axios.post('http://localhost:5000/login',
{
username: data.username,
password: data.password
},
{ withCredentials: true }
)
.then(response => {
this.setState({
currentUser: response.data
})
}
}

This function takes a single argument: the data returned in the promise from our API. The second argument in the axios action is the object of data we are looking to send. Since the state is an object with two keys, you want to structure that object so that Rails can consume it. The way it is structured, on a successful login, we will be returned a promise with json structured like so:

{ user: { username: 'ctd', password: 'ENCRYPTED_SALTED_KEY' },  
logged_in: true
}

Our user key points to an entire user object based on the data we received from the API, so when we build the functionality to retrieve it, it’s already in the formatting we are looking for. Speaking of which, it’s time to update our Home component to do just that.

Inside of Home, go to the handleSubmit function and let’s update it:

handleSubmit = event => {
event.preventDefault();
this.props.login(this.state);
this.setState({
username: '',
password: ''
})
}

Awesome! Now we have the functionality to log in, we need to be able to log out as well. Let’s define handleLogout:

handleLogout = () => {
axios.get('http://localhost:5000/logout', { withCredentials: true})
.then(response => this.setState({ currentUser: undefined}))
}

All this does is fetch to the API to reset the session and then reset App's state to its default state, effectively logging out a user. Of course, don’t forget to pass it as a prop in the route:

<Route exact path={'/dashboard'}
render={props =>
<Dashboard {...props}
currentUser={this.state.currentUser}
logout={this.handleLogout}
/>}
/>

And that’s it for now! Thanks for reading, I hope this was helpful

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store