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.
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:
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.
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:
@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.
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:
-
<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. -
<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. -
<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:
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:
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:
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:
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:
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:
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.
3.3 ResetPassword
Let's keep going with the next and final one, which is the ResetPassword
component.
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.
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
:
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:
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:
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:
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:
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:
const Dashboard = () => { return ( <div> <h2 className="text-3xl font-semibold">Dashboard</h2> </div> ); } export default Dashboard;
The /profile
page:
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.
5. Link pages together
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.
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.
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.
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.
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.
It consists of the following open-source softwares and services:
- A PostgreSQL database
- An instant GraphQL API from Hasura
- Authentication with Hasura Auth
- Storage with Hasura Storage
- Serverless Functions
- The Nhost CLI for local development
- The Nhost SDK for JavaScript, React, and Next.js
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.
Next, give your new Nhost app a name, select a geographic region for your Nhost services and click Create 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.
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:
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:
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:
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.
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:
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.
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.
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.
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.
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.
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:
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.
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:
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.
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.
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:
function ProtectedRoute({ children }) { //... return children; }
Finally, add the Layout
component to your App.js
file by wrapping it inside
the ProtectedRoute
component:
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:
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:
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.
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:
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.
Paste your admin secret and click Enter on the next screen.
You should now get access to your Hasura console. So, go to data, select the users table from the auth folder, and click Permissions.
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.
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.
Finally, select the columns you'd like the users to have access to, and click Save Permissions.
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.
<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.
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.
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:
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> ); };
The Profile
page:
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> ); };
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.
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:
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> ); };
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.
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:
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:
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.
Guest Authors
Write for the AlterClass blog to empower the developer community and grow your brand.
Join usStay up to date
Subscribe to the newsletter to stay up to date with tutorials, courses and much more!