Create your course for free with zero transaction fee →
Back to tutorials

Add authentication to your React app with Nhost

Apr 20, 2022 · 40 min read

Add authentication to your React app with Nhost

Overview

In this tutorial, we are going to learn how we can use React and Nhost to easily add authentication and user management to our app.

We'll start from scratch with the create-react-app command and build a full-stack React app in no time. Indeed, thanks to Nhost simple and powerful serverless backend services, we'll add authentication, a PostgreSQL database, a GraphQL API, a protected dashboard, and user profiles to our app.

At the end of this tutorial, you should get a fully-featured React app coupled with a serverless backend that you could re-use as a starting point for your next projects.

We'll cover:

  • Creating a new React app from scratch
  • Building the UI for the components and pages to handle a complete authentication workflow
  • Setting up and configuring Nhost as a serverless backend to provide a PostgreSQL database and a GraphQL API instantly to our React app
  • Using the Nhost JavaScript SDK for authenticating and managing users
  • Protecting pages of our app with Nhost and React Router
  • Updating user data with GraphQL

You can find the source code for the final version here.

You can also check out the live demo.

Build the React app

1. Create a new React app

Let's start by creating a new React application. The simplest and quickest way to do it is by using the create-react-app CLI, which bootstraps a new React app for us without the hassle of configuring everything ourselves.

So, open your terminal, and run the following command:

npx create-react-app nhost-react-auth

I've named my project nhost-react-auth but feel free to choose whatever name you'd like.

Once your app is created, cd into your project directory:

cd nhost-react-auth/

And run the development server with the following command:

npm run start

Your new React application should now be running on port 3000. Open http://localhost:3000/ from your browser to check this out.

Welcome to React
Tasks

2. Install and configure Tailwind CSS

As we will create the UI for the components and pages of our React app from scratch, we'll use Tailwind CSS to style everything and create a clean and modern design.

So, let's set up Tailwind in our React app.

Start by installing it along with the following dependencies using npm:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

The postcss dependency is necessary here since we'll configure Tailwind inside our project as a PostCSS plugin to generate the CSS of our app based on the Tailwind utility classes we'll use within our JSX.

In addition to Tailwind, we are also installing the autoprefixer PostCSS plugin. It is one of the most popular PostCSS plugins. We'll use it to automatically add vendor prefixes to our CSS so that we don't have to think about it.

Once you have everything installed, the next step is to generate your tailwind.config.js and postcss.config.js files by running the following command:

npx tailwindcss init -p

Running this command creates a minimal Tailwind config file at the root of our project.

We can now add the path to our src/ folder within the content section of our Tailwind configuration object. This path is where our React components will live, and therefore it's where we'll use the Tailwind CSS utility classes.

By doing so, we are telling Tailwind what files to process within our project. In other words, if you omit to add this path to your configuration, Tailwind won't process anything, and therefore the CSS of your app won't be generated.

So, make sure you add the following path to your config file:

tailwind.config.js
module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: {}, }, plugins: [], };

The init command has also created the PostCSS config file already pre-configured with tailwindcss and autoprefixer as plugins. So, we don't need to edit this file.

postcss.config.js
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, };

Finally, make sure to include the Tailwind CSS directives in your main CSS file.

So, open the ./src/index.css file and replace the original content with:

src/index.css
@tailwind base; @tailwind components; @tailwind utilities;

You can also delete any other CSS files that create-react-app generated for us by default, such as the App.css file, as we won't need them. Make sure to remove any dependencies for this file in your project, like in the App.js file.

Alright! We are done with installing and configuring Tailwind CSS in our React application.

Let's keep going with creating our React components.

Tasks

3. Implement the UI of the auth components

Before getting started on the implementation, create the components/ and pages/ folders as this is where we are going to create the components and pages of our application, respectively.

mkdir src/components src/pages

Right now, your folder structure should look like this:

react-nhost-auth/ ├─ public/ | ├─ index.html ├─ src/ | ├─ components/ | ├─ pages/ | ├─ App.js | ├─ index.css | └─ index.js | ├─ .gitignore ├─ package.json ├─ postcss.config.json └─ tailwind.config.json

Even if Nhost (our serverless backend provider) can handle many different authentication mechanisms (email/password, passwordless email and SMS, and OAuth providers), in this tutorial, we'll only allow our users to sign-up and sign-in by using their email and a password.

But don't worry, once you have your Nhost application in place, adding more authentication methods is relatively easy. However, this is out of the scope of this tutorial.

With that being said, let's review the components we need to implement the UI of a proper email/password authentication workflow:

  1. <SignUp />: this component is responsible for rendering the form to collect user's information (first name, last name, email address, and password) on sign-up, and call the corresponding Nhost backend to create a new user account.

  2. <SignIn />: this component is responsible for rendering the form to allow an existing user to sign in to the application using his email and password.

  3. <ResetPassword />: this component is responsible for rendering the form to allow an existing user to ask for the instructions to reset his password.

Of course, we'll also have to create the UI for the pages/routes of our application. But, let's focus on those individual components first.

3.1 SignUp

First, create a new file named SignUp.js inside the components/ folder, and export a React component:

components/SignUp.js
const SignUp = () => { return null; }; export default SignUp;

Great! Now import the useState hook from React as we're going to need it to manage several state variables inside this component.

Actually, we need four state variables, one for each piece of data we want to collect from our users: first name, last name, email, and password.

So, let's go ahead and add state to our component:

components/SignUp.js
import { useState } from 'react'; const SignUp = () => { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); return null; };

Next, let's define the UI of our component with the following JSX code:

components/SignUp.js
import Input from './Input'; const SignUp = () => { // ... return ( <div className="w-full max-w-lg"> <div className="sm:rounded-xl sm:shadow-md sm:border border-opacity-50 sm:bg-white px-4 sm:px-8 py-12 flex flex-col items-center"> <div className="h-14"> <img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" className="w-full h-full" /> </div> <form onSubmit={null} className="w-full"> <div className="mt-12 flex flex-col items-center space-y-6"> <div className="w-full flex gap-6"> <Input label="First name" value={firstName} onChange={e => setFirstName(e.target.value)} required /> <Input label="Last name" value={lastName} onChange={e => setLastName(e.target.value)} required /> </div> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} required /> <Input type="password" label="Create password" value={password} onChange={e => setPassword(e.target.value)} required /> </div> <button type="submit" className="mt-6 w-full font-medium inline-flex justify-center items-center rounded-md p-3 text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:border-bg-600 transition-colors" > Create account </button> </form> </div> </div> ); };

As you can see, our component UI is composed of a logo (feel free to replace it with your own logo) and a form element that renders four input fields.

Each input field uses the corresponding state variables we've defined before to collect the user's information through the onChange event.

Note that I've created a separate <Input /> component for the input elements so that I can re-use it and create a consistent UI throughout the application.

So, make sure to also create this component inside a Input.js file within the components/ folder and use the following code:

components/Input.js
const Input = ({ type = 'text', label = '', ...props }) => { return ( <div className="w-full flex flex-col"> {label ? ( <label className="text-gray-700 font-medium text-sm mb-1"> {label} </label> ) : null} <input type={type} className="w-full shadow-sm rounded-md p-3 border border-gray-300 focus:border-blue-500 focus:ring-blue-500 focus:outline-none focus:ring-4 focus:ring-opacity-20 transition disabled:opacity-50 disabled:cursor-not-allowed read-only:opacity-50 read-only:cursor-not-allowed read-only:focus:border-gray-300 read-only:focus:ring-0" {...props} /> </div> ); }; export default Input;

Now, if you try to render the <SignUp /> component inside your app, this is how it should look like:

SignUp component

Congratulations, you have just created a clean and modern sign-up form for your application.

Note that we have not implemented the onSubmit event handler of our form yet as this is something we'll handle later, after setting up Nhost.

3.2 SignIn

Now that we have created our first authentication component, implementing the other ones will be pretty straightforward.

Indeed, the <SignIn /> component is quite similar to the <SignUp /> one expect that it only needs to handle two state variables, one for the email address and one for the password of the user.

So, create a new file named SignIn.js inside the components/ folder and use the following code:

components/SignIn.js
import { useState } from 'react'; import Input from './Input'; const SignIn = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); return ( <div className="w-full max-w-lg"> <div className="sm:rounded-xl sm:shadow-md sm:border border-opacity-50 sm:bg-white px-4 sm:px-8 py-12 flex flex-col items-center"> <div className="h-14"> <img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" className="w-full h-full" /> </div> <form onSubmit={null} className="w-full"> <div className="mt-12 w-full flex flex-col items-center space-y-6"> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} required /> <Input type="password" label="Password" value={password} onChange={e => setPassword(e.target.value)} required /> </div> <button type="submit" className="mt-6 w-full font-medium inline-flex justify-center items-center rounded-md p-3 text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:border-bg-600 transition-colors" > Sign in </button> </form> </div> </div> ); }; export default SignIn;

Again, this component uses the <Input /> component we've created before to render the input elements.

SignIn component

3.3 ResetPassword

Let's keep going with the next and final one, which is the ResetPassword component.

components/ResetPassword.js
import { useState } from 'react'; import Input from './Input'; const ResetPassword = () => { const [email, setEmail] = useState(''); return ( <div className="w-full max-w-lg"> <div className="sm:rounded-xl sm:shadow-md sm:border border-opacity-50 sm:bg-white px-4 sm:px-8 py-12 flex flex-col items-center"> <div className="h-14"> <img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" className="w-full h-full" /> </div> <h1 className="mt-12 text-2xl font-semibold">Reset your password</h1> <form onSubmit={null} className="w-full"> <div className="mt-12 w-full flex flex-col items-center space-y-6"> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} required /> </div> <button type="submit" className="mt-6 w-full font-medium inline-flex justify-center items-center rounded-md p-3 text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:border-bg-600 transition-colors" > Send reset link </button> </form> </div> </div> ); }; export default ResetPassword;

Same thing as before. We are rendering a form, and this time, with only one input field for an email address that we'll use to send the instructions for resetting the user password.

ResetPassword component
Tasks

4. Set up client-side routing

Now that we've implemented the building blocks of our application, we're going to use them within the different pages of our app (/sign-up, /sign-in, and /reset-password), and set up the navigation between those pages.

To do so, we'll use the React Router library in version 6. It is the most popular client-side (and server-side) routing library for React.

So, let's install react-router-dom into our app using npm:

npm i react-router-dom

Then, inside your App.js file, configure client-side routing by wrapping everything inside the BrowserRouter component provided by react-router-dom:

src/App.js
import { BrowserRouter } from 'react-router-dom'; function App() { return ( <BrowserRouter> {/* ... */} </BrowserRouter> ); } export default App;

The BrowserRouter component is simply a regular React component that uses the HTML5 history API to keep the UI of our application in sync with the current URL.

Next, import the Routes and Route components from react-router-dom and configure the routes of your app as shown below:

src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import SignUp from './pages/SignUp'; import SignIn from './pages/SignIn'; import ResetPassword from './pages/ResetPassword'; import Dashboard from './pages/Dashboard'; import Profile from './pages/Profile'; function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Dashboard />} /> <Route path="sign-up" element={<SignUp />} /> <Route path="sign-in" element={<SignIn />} /> <Route path="reset-password" element={<ResetPassword />} /> <Route path="profile" element={<Profile />} /> </Routes> </BrowserRouter> ); }

Those two components are responsible for rendering the different views/pages of our application.

Each route is defined by a Route component which takes in a path and an element props. The component inside the element prop is the UI React Router will render when the corresponding path value matches the current browser URL.

In our case, we've created five routes. Each of those routes renders a different React component, which represents the UI of the corresponding page.

The pages don't exist yet, but let's go ahead and create them inside the pages/ folder. Remember, those pages are nothing else than regular React components.

The /sign-up page:

pages/SignUp.js
import SignUp from '../components/SignUp'; const SignUpPage = () => { return ( <div className="h-screen flex items-center justify-center py-6"> <SignUp /> </div> ); }; export default SignUpPage;

The /sign-in page:

pages/SignIn.js
import SignIn from '../components/SignIn'; const SignInPage = () => { return ( <div className="h-screen flex items-center justify-center py-6"> <SignIn /> </div> ); }; export default SignInPage;

The /reset-password page:

pages/ResetPassword.js
import ResetPassword from '../components/ResetPassword'; const ResetPasswordPage = () => { return ( <div className="h-screen flex items-center justify-center py-6"> <ResetPassword /> </div> ); }; export default ResetPasswordPage;

The /dashboard page:

pages/Dashboard.js
const Dashboard = () => { return ( <div> <h2 className="text-3xl font-semibold">Dashboard</h2> </div> ); } export default Dashboard;

The /profile page:

pages/Profile.js
import UserProfile from '../components/UserProfile'; const Profile = () => { return <div> <h2 className="text-3xl font-semibold">My profile</h2> </div>; }; export default Profile;

Great! If you now try to access any of the routes we've defined in App.js, you should see the corresponding UI rendered on the screen.

Go ahead and try it by visiting /sign-up or /sign-in for instance.

Tasks

Before going further with the authentication, let's make the navigation between our pages a little bit easier. Indeed, for now, the only way to navigate our app is by manually changing the URL from the browser address bar.

We can do better than that, don't you think?

It is actually relativity easy to do. React Router provides a component named <Link /> which we can render within our components and pages.

Rendering this component allows users to change the URL when they click it. Under the hood, React Router pushes a new entry into the history stack, which results in the location changing and the new route being rendered.

So, let's add inside our SignUp component a link to the /sign-in page.

components/SignUp.js
import { Link } from 'react-router-dom'; const SignUp = () => { //... return ( <div className="w-full max-w-lg"> <div>{/* ... */}</div> <p className="sm:mt-8 text-gray-500 text-center"> Already have an account?{' '} <Link to="/sign-in" className="text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition" > Sign in </Link> </p> </div> ); };

And inside the SignIn component, add a link to the /sign-up page and another link to the /reset-password page.

components/SignIn.js
import { Link } from 'react-router-dom'; const SignIn = () => { //... return ( <div className="w-full max-w-lg"> <div> <form>{/* ... */}</form> <Link to="/reset-password" className="mt-4 text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition" > Forgot your password? </Link> </div> <p className="sm:mt-8 text-gray-500 text-center"> No account yet?{' '} <Link to="/sign-up" className="text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition" > Sign up </Link> </p> </div> ); };

Finally, inside the ResetPassword component, add a link back to the /sign-in page.

components/ResetPassword.js
import { Link } from 'react-router-dom'; const ResetPassword = () => { //... return ( <div className="w-full max-w-lg"> <div>{/* ... */}</div> <p className="sm:mt-8 text-gray-500 text-center"> Already have an account?{' '} <Link to="/sign-in" className="text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition" > Sign in </Link> </p> </div> ); };

That way, a user can navigate back and forth between the /sign-up and /sign-in routes and between the /sign-in and /reset-password routes.

Tasks

Add authentication with Nhost

Now, for probably, the most exciting part of this tutorial, we're about to learn how we can add authentication to our React app with a service called Nhost.

But actually, what is Nhost?

Nhost is a serverless backend for web and mobile applications. In other words, it provides a suite of backend services that you can use out-of-the-box with your apps without needing to manage any infrastructure at all.

Architecture of Nhost
Architecture of Nhost

It consists of the following open-source softwares and services:

Plus, Nhost is framework agnostic which means you can use it with any frontend frameworks/libraries you want, and not just React.

In summary, Nhost makes our life easier and allows us to launch full-stack applications in less time.

1. Create your Nhost project

First things first, we need to create a new Nhost project. So, if you haven't already, create an account on Nhost.io.

Then, log in to your Nhost dashboard and click the Create your first app button.

Nhost - Create your first app
Nhost - Create your first app

Next, give your new Nhost app a name, select a geographic region for your Nhost services and click Create App.

Nhost - Create a new app
Nhost - Create a new app

After a few seconds (yes, that's really fast!) you should get a PostgreSQL database, a GraphQL API for your data, file storage, and more, already set up.

How cool is that?

Make sure to copy your Nhost backend URL as we are doing to need it within our React project.

Nhost - Backend services up and running!
Nhost - Backend services up and running!

2. Set up Nhost with React

Now that we have our new Nhost app up and running, let's go back to our code editor.

To work with Nhost from within our React app, we'll use the React SDK provided by Nhost. It's a wrapper around the Nhost JavaScript SDK which gives us a way to interact with our Nhost backend using React hooks.

You can install the Nhost React SDK with npm:

npm install @nhost/react

Next, open your App.js file as we'll now configure Nhost inside our app.

The Nhost React SDK comes with a React provider named NhostReactProvider that makes the authentication state and all the provided React hooks available in our application.

So, import it at the top of your file and wrap your JSX with this provider component:

src/App.js
import { NhostReactProvider } from '@nhost/react'; function App() { return ( <NhostReactProvider> <BrowserRouter> {/* ... */} </BrowserRouter> </NhostReactProvider> ); }

Now, we need to instantiate a Nhost client and pass it to this provider to interact with our Nhost backend.

For that, create a new file named nhost.js inside a new folder called lib/, and paste the following code:

src/lib/nhost.js
import { NhostClient } from '@nhost/react'; const nhost = new NhostClient({ backendUrl: process.env.REACT_APP_NHOST_BACKEND_URL, }); export { nhost };

This code uses the NhostClient exported from the Nhost React SDK to instantiate a new Nhost client.

Note that we must provide a Nhost backend URL to create this Nhost instance and link it to our Nhost backend. That's precisely the URL we copied earlier in the previous section.

Here, I'm using an environment variable, REACT_APP_NHOST_BACKEND_URL, to store that URL.

So, make sure to create that variable too inside a .env.local file at the root of your project and paste your Nhost backend URL as the value:

.env.local
REACT_APP_NHOST_BACKEND_URL="..."

Finally, go back to your App.js, import your Nhost client from lib/nhost, and pass it to the <NhostReactProvider> component as a prop.

src/App.js
import { NhostReactProvider } from '@nhost/react'; import { nhost } from './lib/nhost'; function App() { return ( <NhostReactProvider nhost={nhost}> {/* ... */} </NhostReactProvider> ); }

We are now all set! That's all it takes to set up and configure Nhost inside your React application.

3. Sign-up users

Great! The next step is to allow our users to sign-up and sign-in to our application. Let's start with implementing the sign-up process.

For that, we'll use the useSignUpEmailPassword hook provided by the Nhost React SDK.

import { useSignUpEmailPassword } from '@nhost/react';

This React hook accepts as an argument an optional options object to set some information about the user we'd like to sign up to our application, such as his display name or his default role.

And it returns a method called signUpEmailPassword which, as its name suggests, allows us to sign up a new user with email and password. Also, it returns a couple of variables to keep track of the sign-up process status inside our React component.

const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error, } = useSignUpEmailPassword(options);

We'll use the useSignUpEmailPassword hook within our SignUp component. So, open up the corresponding file from your project, import the hook at the top, and call it inside your component:

components/SignUp.js
import { useSignUpEmailPassword } from '@nhost/react'; const SignUp = () => { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error, } = useSignUpEmailPassword({ displayName: `${firstName} ${lastName}`.trim(), metadata: { firstName, lastName, }, }); //... };

As you can see, I'm using the state variables we've defined before to populate the displayName and metadata options that will be used when creating a new user with Nhost.

It's important to understand that calling the useSignUpEmailPassword hook does NOT sign up the user yet. Indeed, we must also call the signUpEmailPassword method returned by that hook to sign up the user.

Here, the best time to do it is when the user submits the sign-up form after entering his details (name, email, password).

So, create a new handler function named handleOnSubmit and call the signUpEmailPassword method inside that function by passing in the email and password values. Then, pass the handleOnSubmit function through the onSubmit prop of the form element.

components/SignUp.js
const SignUp = () => { //... const handleOnSubmit = e => { e.preventDefault(); signUpEmailPassword(email, password); }; return ( <div> <form onSubmit={handleOnSubmit}> {/* ... */} </form> </div> ); };

Finally, let's use all the other variables returned by the hook to adapt our UI depending on the sign-up process status.

components/SignUp.js
import { Link, Navigate } from 'react-router-dom'; const SignUp = () => { //... if (isSuccess) { return <Navigate to="/" replace={true} />; } const disableForm = isLoading || needsEmailVerification; return ( <div className="w-full max-w-lg"> <div className="..."> {/* ... */} {needsEmailVerification ? ( <p className="mt-12 text-center"> Please check your mailbox and follow the verification link to verify your email. </p> ) : ( <form onSubmit={handleOnSubmit} className="w-full"> <div className="..."> <div className="..."> <Input label="First name" value={firstName} onChange={e => setFirstName(e.target.value)} disabled={disableForm} required /> <Input label="Last name" value={lastName} onChange={e => setLastName(e.target.value)} disabled={disableForm} required /> </div> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} disabled={disableForm} required /> <Input type="password" label="Create password" value={password} onChange={e => setPassword(e.target.value)} disabled={disableForm} required /> </div> <button type="submit" disabled={disableForm} className="..." > {isLoading ? 'Loading...' : 'Create account'} </button> {isError ? ( <p className="mt-4 text-red-500 text-center">{error?.message}</p> ) : null} </form> )} </div> {/* ... */} </div> ); };

If the user has been successfully signed up, we redirect him to the homepage of our app by checking the isSuccess variable and using the <Navigate> component from React Router.

We also disable all the input fields and the submit button while the user is being signed up or needs to verify his email address.

Finally, we display a message on the screen if any error occurs during the sign-up process, and if the user needs to verify his email address to complete the sign-up process.

Note that, by default, the user must verify his email address before being fully signed up. You can change this setting from your Nhost dashboard, as shown in the screenshot below.

Nhost - Only allow login for verified emails
Nhost - Only allow login for verified emails

Alright! Now try to sign up for your application from here: localhost:3000/sign-up.

After submitting the form with your information, you should receive a verification email from Nhost, like the one in the screenshot below.

Nhost - Verfication email
Nhost - Verfication email

Click the link from that email, and then, if everything is successful, you should be fully signed up and redirected to the homepage of the application.

And if you look at your Nhost dashboard, you should see a new entry in the User accounts section.

Nhost - New user created
Nhost - New user created

As you can see, just by using the useSignUpEmailPassword hook from the Nhost SDK, we are able to sign-up users to our app and create a new account in our Nhost backend.

It's as simple as that. Nhost is handling all the authentication logic for us and provide and manage the backend infrastructure to store our user accounts.

4. Sign-in users

Now that new users can sign up for our application, let's see how to allow existing users to sign in with email and password.

If we look at the official documentation, we can see that Nhost also provides a React hook for that.

It is named useSignInEmailPassword and has the following signature:

const { signInEmailPassword, isLoading, needsEmailVerification, isSuccess, isError, error, } = useSignInEmailPassword();

Similar to the sign-up hook, useSignInEmailPassword returns a method we can call to sign in a user using his email and password, and a few other variables to keep track of the sign-in status. It does not take in any arguments, though.

As you may have already guessed, we will use that hook inside our SignIn component the same way we did with our SignUp component. So, here's what your component should look like after applying the changes for the sign-in logic:

components/SignIn.js
import { useSignInEmailPassword } from '@nhost/react'; import { Link, Navigate } from 'react-router-dom'; const SignIn = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const { signInEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error, } = useSignInEmailPassword(); const handleOnSubmit = e => { e.preventDefault(); signInEmailPassword(email, password); }; if (isSuccess) { return <Navigate to="/" replace={true} />; } const disableForm = isLoading || needsEmailVerification; return ( <div className="..."> <div className="..."> {/* ... */} {needsEmailVerification ? ( <p className="mt-12 text-center"> Please check your mailbox and follow the verification link to verify your email. </p> ) : ( <> <form onSubmit={handleOnSubmit} className="w-full"> <div className="..."> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} disabled={disableForm} required /> <Input type="password" label="Password" value={password} onChange={e => setPassword(e.target.value)} disabled={disableForm} required /> </div> <button type="submit" disabled={disableForm} className="..." > {isLoading ? 'Loading...' : 'Sign in'} </button> {isError ? ( <p className="mt-4 text-red-500 text-center"> {error?.message} </p> ) : null} </form> {/* ... */} </> )} </div> </div> ); };

5. Protect routes

So far, we have not protected any routes of our application. In other words, whether or not a user is authenticated, he can access any pages he'd like.

So you may wonder what's the point of adding authentication if anyone can access everything in our application?

And you are right. But now that we have implemented email/password authentication, we can easily decide who can access certain parts of our application. In our case, we'll only allow authenticated users to have access to the / and /profile routes. All the other users should be redirected to the /sign-in page if they try to access those routes.

To do so, we can create a wrapper component (ProtectedRoute) to check the authentication status of the current user (logged in or not) using the Nhost SDK.

components/ProtectedRoute.js
import { useAuthenticationStatus } from '@nhost/react'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; function ProtectedRoute() { const { isAuthenticated, isLoading } = useAuthenticationStatus(); const location = useLocation(); if (isLoading) { return <div>Loading...</div>; } if (!isAuthenticated) { return <Navigate to="/sign-in" state={{ from: location }} replace />; } return <Outlet />; } export default ProtectedRoute;

As you can see, the ProtectedRoute component uses the useAuthenticationStatus hook from Nhost to check if the current user is authenticated.

If not, we redirect him to the /sign-in page using the Navigate component of React Router as he must authenticate himself before being able to access a protected route.

Otherwise, if he is already authenticated, we render the corresponding route using the Outlet component, also provided by React Router.

Then, we can use a layout route in our App.js file, to wrap the ProtectedRoute component around the routes we want to protect:

src/App.js
import ProtectedRoute from './components/ProtectedRoute'; function App() { return ( <NhostReactProvider nhost={nhost}> <BrowserRouter> <Routes> <Route path="sign-up" element={<SignUp />} /> <Route path="sign-in" element={<SignIn />} /> <Route path="reset-password" element={<ResetPassword />} /> <Route path="/" element={<ProtectedRoute />}> <Route index element={<Dashboard />} /> <Route path="profile" element={<Profile />} /> </Route> </Routes> </BrowserRouter> </NhostReactProvider> ); }

6. Read user data

If you look at the / or the /profile pages, you should find them a bit boring. Indeed, almost nothing is displayed on the screen, apart from a single header.

The empty dashboard UI

So, it's now time to implement those pages and customize them with the current authenticated user data.

We'll start by creating a new component (Layout) to use for the / and the /profile pages so that they can share a common layout.

Start by creating this component under the components/ folder, and render the Outlet from React Router as it will act as our layout route now.

components/Layout.js
import { Outlet } from 'react-router-dom'; const Layout = () => { return <Outlet />; }; export default Layout;

Then, edit the ProtectedRoute component and replace Outlet by the children prop:

/components/ProtectedRoute
function ProtectedRoute({ children }) { //... return children; }

Finally, add the Layout component to your App.js file by wrapping it inside the ProtectedRoute component:

src/App.js
import Layout from './components/Layout'; function App() { return ( <NhostReactProvider nhost={nhost}> <BrowserRouter> <Routes> {/* ... */} <Route path="/" element={ <ProtectedRoute> <Layout /> </ProtectedRoute> } > <Route index element={<Dashboard />} /> <Route path="profile" element={<Profile />} /> </Route> </Routes> </BrowserRouter> </NhostReactProvider> ); }

Both the Dashboard and the Profile pages share the same layout.

Regarding the UI of our Layout component, use the following code:

components/Layout.js
import { Fragment } from 'react'; import { Outlet, Link } from 'react-router-dom'; import { Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon, HomeIcon, LogoutIcon, UserIcon, } from '@heroicons/react/outline'; const Avatar = ({ src = '', alt = '' }) => ( <div className="rounded-full bg-gray-100 overflow-hidden w-9 h-9"> {src ? <img src={src} alt={alt} /> : null} </div> ); const Layout = () => { const menuItems = [ { label: 'Dashboard', href: '/', icon: HomeIcon, }, { label: 'Profile', href: '/profile', icon: UserIcon, }, { label: 'Logout', onClick: () => null, icon: LogoutIcon, }, ]; return ( <div> <header className="fixed z-10 top-0 inset-x-0 h-[60px] shadow bg-white"> <div className="container mx-auto px-4 py-3 flex justify-between"> <Link to="/"> <img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" /> </Link> <Menu as="div" className="relative z-50"> <Menu.Button className="flex items-center space-x-px group"> <Avatar src="" alt="" /> <ChevronDownIcon className="w-5 h-5 shrink-0 text-gray-500 group-hover:text-current" /> </Menu.Button> <Transition as={Fragment} enter="transition ease-out duration-100" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="transition ease-in duration-75" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > <Menu.Items className="absolute right-0 w-72 overflow-hidden mt-1 divide-y divide-gray-100 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <div className="flex items-center space-x-2 py-4 px-4 mb-2"> <div className="shrink-0"> <Avatar src="" alt="" /> </div> <div className="flex flex-col truncate"> <span>Elon Musk</span> <span className="text-sm text-gray-500"> elon@spacex.com </span> </div> </div> <div className="py-2"> {menuItems.map(({ label, href, onClick, icon: Icon }) => ( <div key={label} className="px-2 last:border-t last:pt-2 last:mt-2" > <Menu.Item> {href ? ( <Link to={href} className="flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100" > <Icon className="w-5 h-5 shrink-0 text-gray-500" /> <span>{label}</span> </Link> ) : ( <button className="w-full flex items-center space-x-2 py-2 px-4 rounded-md hover:bg-gray-100" onClick={onClick} > <Icon className="w-5 h-5 shrink-0 text-gray-500" /> <span>{label}</span> </button> )} </Menu.Item> </div> ))} </div> </Menu.Items> </Transition> </Menu> </div> </header> <main className="mt-[60px]"> <div className="container mx-auto px-4 py-12"> <Outlet /> </div> </main> </div> ); };

Note that Layout uses the @headlessui and the @heroicons libraries for the dropdown menu and the icons. So, make sure to install them into your project:

npm i @headlessui/react @heroicons/react

Your dashboard should now look like this:

The dashboard UI

Another great feature of Nhost is the GraphQL API that comes out of the box.

Nhost provides this GraphQL API through Hasura, an open-source product that instantly generates a GraphQL API with built-in authorization from our data.

This is very handy as we can query and mutate our data without worrying about setting up an API ourselves or the corresponding server infrastructure.

On top of that, we can connect to this API with any GraphQL client we'd like.

In this tutorial, we'll use the Apollo GraphQL client, but feel free to use another library if you prefer.

The Nhost SDK even comes with its own GraphQL client. Check it out from the official documentation.

So, start by installing the following dependencies:

npm install @nhost/react-apollo @apollo/client

Then, add the NhostApolloProvider from @nhost/react-apollo into your App.js file. Make sure this provider is nested into NhostReactProvider, as it will need the Nhost context.

src/App.js
import { NhostApolloProvider } from '@nhost/react-apollo'; function App() { return ( <NhostReactProvider nhost={nhost}> <NhostApolloProvider nhost={nhost}> {/* ... */} </NhostApolloProvider> </NhostReactProvider> ); }

From there, we can construct the query to retrieve the current authenticated user data using GraphQL. We will do that from within the Layout component so that we can share the data with its children, the Dashboard and the Profile pages.

Below is the query to retrieve the user data:

components/Layout.js
import { useUserId, signOut } from '@nhost/react'; import { gql, useQuery } from '@apollo/client'; const GET_USER_QUERY = gql` query GetUser($id: uuid!) { user(id: $id) { id email displayName metadata avatarUrl } } `; const Layout = () => { const id = useUserId(); const { signOut } = useSignOut(); const { loading, data } = useQuery(GET_USER_QUERY, { variables: { id }, }); const user = data?.user; const menuItems = [ //... { label: 'Logout', onClick: signOut, icon: LogoutIcon, }, ]; if (loading) return 'Loadiing...'; //... }

Here, we are using the useUserId hook from Nhost to get the user's ID and the useQuery from Apollo to run the query, GET_USER_QUERY.

Note that we are also using the Nhost useSignOut hook to allow the user to sign out from the app from the dropdown menu.

It's important to understand that Hasura has an allow nothing by default policy to ensure that only roles and permissions you define explicitly have access to the GraphQL API. For now, our query does not return anything because we haven't set any permissions in Hasura yet.

So, let's go back to the Nhost dashboard, and from the data tab, copy your admin secret and click Open Hasura.

Nhost - Copy your admin secret and open Hasura
Nhost - Copy your admin secret and open Hasura

Paste your admin secret and click Enter on the next screen.

Nhost - Paste your admin secret to enter Hasura
Nhost - Paste your admin secret to enter Hasura

You should now get access to your Hasura console. So, go to data, select the users table from the auth folder, and click Permissions.

Nhost - User permissions with Hasura (1)
Nhost - User permissions with Hasura (1)

Hasura supports role-based access control. In other words, you can create rules for each role, table, and operation (select, insert, update and delete) that can check dynamic session variables, like the user ID.

In our case, we need to add permissions on the select operation for the user role.

Nhost - User permissions with Hasura (2)
Nhost - User permissions with Hasura (2)

To restrict the user to read his own data only, specify a condition with the user's ID and the X-Hasura-User-ID session variable, which is passed with our requests.

Nhost - User permissions with Hasura (3)
Nhost - User permissions with Hasura (3)

Finally, select the columns you'd like the users to have access to, and click Save Permissions.

Nhost - User permissions with Hasura (4)
Nhost - User permissions with Hasura (4)

Now, repeat the same steps to add the same permissions on the update operation for the user role as we'll need that later.

Once you are done, go back to your code editor and update the Layout UI to display the user picture, name, and email in the dropdown menu, as shown below.

components/Layout.js
<header className="..."> <div className="..."> {/* ... */} <Menu as="div" className="..."> <Menu.Button className="..."> <Avatar src={user?.avatarUrl} alt={user?.displayName} /> <ChevronDownIcon className="..." /> </Menu.Button> <Transition> <Menu.Items className="..."> <div className="..."> <div className="..."> <Avatar src={user?.avatarUrl} alt={user?.displayName} /> </div> <div className="..."> <span>{user?.displayName}</span> <span className="...">{user?.email}</span> </div> </div> {/* ... */} </Menu.Items> </Transition> </Menu> </div> </header>

If you've carefully followed all the steps so far, you should now see the dropdown menu of your app populated with the current authenticated user data.

Nhost - User menu with data

To pass our user data to the different pages, we can use the Outlet context which is simply a built-in React Context provided by React Router for convenience.

components/Layout.js
const Layout = () => { //... return ( <div> {/* ... */} <main className="..."> <div className="..."> <Outlet context={{ user }} /> </div> </main> </div> ); };

That way, we can now use the useOutletContext to access the user data from our pages.

The Dashboard page:

pages/dashboard.js
import { useOutletContext } from 'react-router-dom'; const Dashboard = () => { const { user } = useOutletContext(); return ( <div> <h2 className="text-3xl font-semibold">Dashboard</h2> <p className="mt-2 text-lg"> Welcome, {user?.metadata?.firstName || 'stranger'}{' '} <span role="img" alt="hello">👋</span> </p> </div> ); };
Nhost - The dashboard page

The Profile page:

pages/profile.js
import { useState } from 'react'; import { useOutletContext } from 'react-router-dom'; import Input from '../components/Input'; const Profile = () => { const { user } = useOutletContext(); const [firstName, setFirstName] = useState(user?.metadata?.firstName ?? ''); const [lastName, setLastName] = useState(user?.metadata?.lastName ?? ''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); return ( <div className="space-y-12"> <div className="flex flex-col lg:flex-row lg:justify-between gap-4 lg:gap-8"> <div className="sm:min-w-[320px]"> <h2 className="text-lg sm:text-xl">Profile</h2> <p className="mt-1 text-gray-500 leading-tight"> Update your personal information. </p> </div> <div className="rounded-md shadow-md border border-opacity-50 w-full max-w-screen-md overflow-hidden bg-white"> <form onSubmit={null}> <div className="px-4 md:px-8 py-6 space-y-6"> <div className="flex flex-col sm:flex-row gap-6"> <Input type="text" label="First name" value={firstName} onChange={e => setFirstName(e.target.value)} required /> <Input type="text" label="Last name" value={lastName} onChange={e => setLastName(e.target.value)} required /> </div> <div className="sm:max-w-md"> <Input type="email" label="Email address" value={user?.email} disabled={true} readOnly /> </div> </div> <div className="w-full bg-gray-50 py-4 px-4 md:px-8 flex justify-end"> <button type="submit" className="bg-gray-700 text-white py-2 px-4 rounded-md focus:outline-none focus:ring-4 focus:ring-gray-700 focus:ring-opacity-20 hover:bg-gray-600 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-gray-700" > Update </button> </div> </form> </div> </div> </div> ); };
Nhost - The profile page

7. Update user data

Using the same GraphQL API we can also mutate our data. That way, we can give our users the ability to update their profile information.

The GraphQL mutation to do it is the following:

const UPDATE_USER_MUTATION = gql` mutation ($id: uuid!, $displayName: String!, $metadata: jsonb) { updateUser( pk_columns: { id: $id } _set: { displayName: $displayName, metadata: $metadata } ) { id displayName metadata } } `;

It takes in as arguments the user's ID and the data we'd like to mutate, displayName and metadata.

You can run this mutation request from the Profile component using the useMutation hook from Apollo when the user submits the form, as shown below.

pages/profile.js
import { gql, useMutation } from '@apollo/client'; const UPDATE_USER_MUTATION = gql` mutation ($id: uuid!, $displayName: String!, $metadata: jsonb) { updateUser( pk_columns: { id: $id } _set: { displayName: $displayName, metadata: $metadata } ) { id displayName metadata } } `; const Profile = () => { //... const [mutateUser, { loading: updatingProfile }] = useMutation(UPDATE_USER_MUTATION); const updateUserProfile = async e => { e.preventDefault(); mutateUser({ variables: { id: user.id, displayName: `${firstName} ${lastName}`.trim(), metadata: { firstName, lastName, }, }, }); }; return ( <div className="..."> <div className="..."> {/* ... */} <div className="..."> <form onSubmit={updateUserProfile}> <div className="..."> <div className="..."> <Input type="text" label="First name" value={firstName} onChange={e => setFirstName(e.target.value)} disabled={updatingProfile} required /> <Input type="text" label="Last name" value={lastName} onChange={e => setLastName(e.target.value)} disabled={updatingProfile} required /> </div> <div className="..."> <Input type="email" label="Email address" value={user?.email} disabled={true} readOnly /> </div> </div> <div className="..."> <button type="submit" disabled={updatingProfile} className="..." > Update </button> </div> </form> </div> </div> </div> ); };

8. Reset password

Great! Now what if a user can't remember his password and therefore can't access his dashboard anymore?

In that case, we must provide a way for our users to reset their passwords. Otherwise, our support team will be swamped with messages of people who can't access our application anymore because they lost their password.

Lucky for us, Nhost also has a React hook for that, called useResetPassword.

Let's use it within our ResetPassword component:

components/ResetPassword.js
import { useResetPassword } from '@nhost/react'; const ResetPassword = () => { const [email, setEmail] = useState(''); const { resetPassword, isLoading, isSent, isError, error } = useResetPassword(); const handleOnSubmit = e => { e.preventDefault(); resetPassword(email, { redirectTo: '/profile' }); }; return ( <div className="..."> <div className="..."> {/* ... */} {isSent ? ( <p className="mt-6 text-center"> An email has been sent to <b>{email}</b>. Please follow the link in the email to reset your password. </p> ) : ( <form onSubmit={handleOnSubmit} className="..."> <div className="..."> <Input type="email" label="Email address" value={email} onChange={e => setEmail(e.target.value)} disabled={isLoading} required /> </div> <button type="submit" disabled={isLoading} className="..." > {isLoading ? 'Loading...' : 'Send reset link'} </button> {isError ? ( <p className="mt-4 text-red-500 text-center">{error?.message}</p> ) : null} </form> )} </div> {/* ... */} </div> ); };
Nhost - The Reset password page

So, what happens when we call the resetPassword method returned by the useResetPassword hook?

If the email address is associated with an account in our database, the corresponding user should receive an email with a temporary connection link.

Nhost - The Reset password email

By clicking this link, the user should be automatically authenticated and redirected to his profile page.

From there, to allow the user to change his password, we need to add a new form to our page:

pages/Profile.js
const Profile = () => { //... const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const isPasswordFormValid = password !== '' && password === confirmPassword; return ( <div className="space-y-12"> {/* ... */} <div className="flex flex-col lg:flex-row lg:justify-between gap-4 lg:gap-8"> <div className="sm:min-w-[320px]"> <h2 className="text-lg sm:text-xl">Password</h2> <p className="mt-1 text-gray-500 leading-tight"> Change your password. </p> </div> <div className="rounded-md shadow-md border border-opacity-50 w-full max-w-screen-md overflow-hidden bg-white"> <form onSubmit={null}> <div className="px-4 md:px-8 py-6 space-y-6"> <Input type="password" label="New password" value={password} onChange={e => setPassword(e.target.value)} required /> <Input type="password" label="Confirm password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} required /> </div> <div className="w-full bg-gray-50 py-4 px-4 md:px-8 flex justify-end"> <button type="submit" disabled={!isPasswordFormValid} className="bg-gray-700 text-white py-2 px-4 rounded-md focus:outline-none focus:ring-4 focus:ring-gray-700 focus:ring-opacity-20 hover:bg-gray-600 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-gray-700" > Change password </button> </div> </form> </div> </div> </div> ); };

And then use the useChangePassword hook from Nhost to reset the user's password with the new one:

pages/Profile.js
import { useChangePassword } from '@nhost/react'; const Profile = () => { const { changePassword, isLoading: updatingPassword } = useChangePassword(); const updatePassword = async e => { e.preventDefault(); const { isError, isSuccess } = await changePassword(password); if (isError) { alert('Unable to update password'); } else if (isSuccess) { setPassword(''); setConfirmPassword(''); } }; return ( <div className="..."> {/* ... */} <form onSubmit={updatePassword}> <div className="..."> <Input type="password" label="New password" value={password} onChange={e => setPassword(e.target.value)} disabled={updatingPassword} required /> <Input type="password" label="Confirm password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} disabled={updatingPassword} required /> </div> <div className="..."> <button type="submit" disabled={!isPasswordFormValid || updatingPassword} className="..." > {updatingPassword ? 'Updating...' : 'Change password'} </button> </div> </form> </div> ); };

Wrapping up

This tutorial demonstrates how, with little effort and thanks to Nhost, you can add authentication to your React app, manage your users' data through the provided GraphQL API, and even set custom permissions on your data from Hasura.

Feel free to re-use the application you have built here as the starting point for your future projects.

You can also clone the source code from the Github repository.

And, if you'd like to go deeper into Nhost, check out the official documentation: https://docs.nhost.io. There is a lot more than just authentication!

If you have any feedback, questions, or just want to share what you've built - I'd love to hear from you. Reach out to me on Twitter at @gdangel0 or @AlterClassIO.

Share

Guest Authors

Write for the AlterClass blog to empower the developer community and grow your brand.

Join us

Stay up to date

Subscribe to the newsletter to stay up to date with tutorials, courses and much more!