Authenticate User with Devise Gem and Devise JWT in React Application (2/2)

Villy Siu
5 min readSep 14, 2022

--

In the previous post, we have built our API in Rails with Devise gem for user authentication, and setup Devise JWT to transmit securely information in JSON object . Now it is time to build the frontend to receive the JWT token. The user interface is simple because I just want to show you how the client communicate with the API.

Return to the root folder, react-user-authenitication, and create the react app.

cd .. 
npx create-react-app frontend
cd frontend

Configure Port

Our API is running in 3000 port. To avoid conflict, we will set the react default port to 3001. Create a new file,.env, in the frontend folder, add the following line.

# frontend/.envPORT=3001

Now if you run

npm start

it will start on localhost:3001.

Setup User Interface

I am going to keep this simple. We will start with App.js. In it we have

  • a currUser created with useState hook. It is initialized as null and will be updated after we have succesfully signed up or logged in.
  • PrivateText , a compnent to fetch the message.
  • User, a component to handle user signup and login, as well as logout upon successful login.
#frontend/src/App.js
import { useState } from 'react';
import './App.css';
import User from './components/User'
import PrivateText from './components/PrivateText'
const App=()=>{
const [currUser, setCurrUser]=useState(null);
return (
<div className="App">
<User currUser={currUser} setCurrUser={setCurrUser} />
</div>
);
}
export default App;

In User.js, we have 3 components, Signup, Login, and Logout. The transmission of the JWT token will happen within the fetch method inside these components.

We passed in the setCurrUser function into them to update currUser state when we succesfully logged in and logged out.

# frontend/src/components/User.js
import Signup from "./Signup";
import Login from './Login'
import Logout from './Logout'
import { useState } from "react";
const User = ({currUser, setCurrUser}) => {
const [show, setShow]=useState(true)
if(currUser)
return (
<div>
Hello {currUser.email}
<PrivateText currUser={currUser}/>
<Logout setCurrUser={setCurrUser}/>
</div>
)
return (
<div>
{ show?
<Login setCurrUser={setCurrUser} setShow={setShow}/>
:
<Signup setCurrUser={setCurrUser} setShow={setShow} />
}
</div>
)
}
export default User

Signup.js

In the Signup.js, after we collected the user email and password in the form, we will wrap the user info in the following format. It is important to do so, because this is how the devise setup to receive.

"user":{ email: data.email, password: data.password }

Then we will make a post request to http://localhost:3000/signup. After devise has successfully created a new user in API, the new user will be signed in (a new session is created). In the client, we will receive a response in JSON object with status 200 success . Along with the response, in the header is the JWT token in Bearer #{token} format.

We can collect the token by response.headers.get("Authorization"). Then we will store it in localstorage.

localStorage.setItem('token',response.headers.get("Authorization"))

currUser will get updated by setCurrUser with the newly created user object.

#frontend/src/components/Signup.jsimport { useRef } from "react"
const Signup=({setCurrUser, setShow})=>{
const formRef = useRef()
const signup=async (userInfo, setCurrUser)=>{
const url="http://localhost:3000/signup"
try{
const response=await fetch(url, {
method: 'post',
headers: {
"content-type": 'application/json',
"accept": "application/json"
},
body: JSON.stringify(userInfo)
})
const data=await response.json()
if(!response.ok) throw data.error
localStorage.setItem('token', response.headers.get("Authorization"))
setCurrUser(data)
} catch (error){
console.log("error", error)
}
}
const handleSubmit=e=>{
e.preventDefault()
const formData=new FormData(formRef.current)
const data=Object.fromEntries(formData)
const userInfo={
"user":{ email: data.email, password: data.password }
}

signup(userInfo, setCurrUser)
e.target.reset()
}
const handleClick=e=>{
e.preventDefault()
setShow(true)
}
return(
<div>
<form ref={formRef} onSubmit={handleSubmit}>
Email: <input type="email" name='email' placeholder="email" />
<br/>
Password: <input type="password" name='password' placeholder="password" />
<br/>
<input type='submit' value="Submit" />
</form>
<br />
<div>Already registered, <a href="#login" onClick={handleClick} >Login</a> here.</div>
</div>
)
}
export default Signup

Login.js

Login is similar to signup. We make a post request to http://localhost:3000/login. User inputs are wrapped in the format devise is designed to receive. Upon successful login, we will store the JWT token in localstorage, and update the user by setCurrUser.

# frontend/src/components/Login.js
import { useRef } from "react"
const Login = ({setCurrUser, setShow}) =>{
const formRef=useRef()
const login=async (userInfo, setCurrUser)=>{
const url="http://localhost:3000/login"
try{
const response=await fetch(url, {
method: "post",
headers: {
'content-type': 'application/json',
'accept': 'application/json'
},
body: JSON.stringify(userInfo)
})
const data=await response.json()
if(!response.ok)
throw data.error
localStorage.setItem("token", response.headers.get("Authorization"))
setCurrUser(data)
}catch(error){
console.log("error", error)
}
}
const handleSubmit=e=>{
e.preventDefault()
const formData=new FormData(formRef.current)
const data=Object.fromEntries(formData)
const userInfo={
"user":{ email: data.email, password: data.password }
}

login(userInfo, setCurrUser)
e.target.reset()
}
const handleClick=e=>{
e.preventDefault()
setShow(false)
}
return(
<div>
<form ref={formRef} onSubmit={handleSubmit}>
Email: <input type="email" name='email' placeholder="email" />
<br/>
Password: <input type="password" name='password' placeholder="password" />
<br/>
<input type='submit' value="Login" />
</form>
<br />
<div>Not registered yet, <a href="#signup" onClick={handleClick} >Signup</a> </div>
</div>
)
}
export default Login

Logout.js

In Logout.js, we are sending a “Delete” request to http://localhost:3000/logout for devise to destroy the current session. To do so, we have to be prove that there is a current session going on by sending in the JWT token in the header from localstorage.

"authorization": localStorage.getItem("token")

Upon successful logout, we will receive a 200 success respone. We will the remove the token from localstoarge and update currUser state to null

# frontend/src/components/Logout
const Logout =({setCurrUser})=>{
const logout=async (setCurrUser)=>{
try {
const response=await fetch("http://localhost:3000/logout",{
method: "delete",
headers: {
"content-type": "application/json",
"authorization": localStorage.getItem("token")
},
})
const data=await response.json()
if(!response.ok) throw data.error
localStorage.removeItem("token")
setCurrUser(null)

} catch (error) {
console.log("error", error)
}
}
const handleClick=e=>{
e.preventDefault()
logout(setCurrUser)
}
return (
<div>
<input type="button" value='Logout' onClick={handleClick}/>
</div>
)
}
export default Logout

PrivateText.js

Now we have covered the user functions. Time to setup the PrivateText component. The private message can only be seen by logged in user. We have a message constant created with useState hook. We will send a GET request to http://localhost:3000/private/test. Like in logout.js, we are sending the JWT token stored in localstorage to the API as well for user authentication. If the token is valid (i.e. user is logged in) and has not expired yet, we will receive a 200 success response, along with the secret message in JSON format. The message will be updated by setMessage and displayed on the screen.

# frontend/src/components/PrivateText.js
import { useState,useEffect } from "react"
const PrivateText=({currUser})=>{
const [message, setMessage]=useState(null)
const getText=async ()=>{
try {
const response=await fetch("http://localhost:3000/private/test", {
method: "get",
headers: {
"content-type": "application/json",
"authorization": localStorage.getItem("token")
}
})
if(!response.ok) throw Error
const data=await response.json()
setMessage(data.message)
}
catch(error){
console.log("error", error)
setMessage(null)
}
}
useEffect(()=>{
if(currUser)
getText()
},[currUser])
return(
<div>{message}</div>
)
}
export default PrivateText

Error handling

I did not include any error handling on the frontend. Error message is logged in the console.

  • If user failed to login, maybe wrong password, we will receive a 401 unauthorized access response.
  • During signup, if email already in use, we will receive a 500 (Internal Server Error).

These errors can be easily displayed on the screen too.

The end

In this two part tutorial, we have covered setting up API in rails with devise gem and devise-JWT gem, and setting up frontend with React. React communicates with API by sending async request. Ihope you will find this useful. A fully working version in available in github, if you are interested in checking it out.

https://github.com/villysiu/react-user-authenication

Happy coding!

--

--