Skip to main content

Quickstart: React

Introduction

This quickstart guide provides the steps you need to build a simple React app powered by Nhost for the backend. It includes:

By the end of this guide, you'll have a full-stack app that allows users to log in to access a protected dashboard and update their profile information.

Prerequisites

Before getting started, let's make sure that your development environment is ready.

You'll need Node.js version 14 or later: install it from here.

Project setup

Create a new Nhost project

First things first, we need to create a new Nhost project.

So, log in to your Nhost dashboard and click the Create Your First Project button.

Nhost Dashboard

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

New Nhost project

After a few seconds, you should get a PostgreSQL database, a GraphQL API with Hasura, file storage, and authentication set up.

info

You can also connect your Nhost project to a Git repository at GitHub. When you do this, any updates you push to your code will automatically be deployed. Learn more

Initialize the app

Create a React app

The simplest way to create a new React application is by using the tool called create-react-app, which bootstraps a React app for you without the hassle of configuring everything yourself.

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

npx create-react-app my-nhost-app --template nhost-quickstart
info

This command uses an existing template, through the --template flag, which already contains the React components and pages we'll use for this guide.

You can now cd into your project directory:

cd my-nhost-app

And run the development server with the following command:

npm start

If everything is working fine, your React development server should be running on port 3000. Open http://localhost:3000 from your browser to check this out.

Configure Nhost with React

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 install @nhost/react graphql

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 NhostProvider that makes the authentication state and all the provided React hooks available in our application.

Use the following code to instantiate a new Nhost client and link it to your Nhost backend:

src/App.js
import { NhostClient, NhostProvider } from '@nhost/react'

const nhost = new NhostClient({
subdomain: process.env.REACT_APP_NHOST_SUBDOMAIN,
region: process.env.REACT_APP_NHOST_REGION
})

function App() {
return (
<NhostProvider nhost={nhost}>
<BrowserRouter>{/* ... */}</BrowserRouter>
</NhostProvider>
)
}

export default App

Finally, make sure to create an environment variable named REACT_APP_NHOST_SUBDOMAIN and REACT_APP_NHOST_REGION to store your Nhost domain details:

.env.local
REACT_APP_NHOST_SUBDOMAIN=[subdomain]
REACT_APP_NHOST_REGION=[region]

You find your Nhost project's subdomain and region in the project overview:

Project Overview

caution

Don't forget to restart your React server after saving your .env.local file to load your new environment variable.

Nhost CLI

Do you use the Nhost CLI? Learn how to set subdomain and region in the CLI documentation.

Build the app

Add authentication

1. Sign-up

The next step is to allow our users to authenticate into 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 within our SignUp component.

So, open up the corresponding file from your project, and use the following code:

src/components/SignUp.js
import styles from '../styles/components/SignUp.module.css'
import { useState } from 'react'
import { useSignUpEmailPassword } from '@nhost/react'
import { Link, Navigate } from 'react-router-dom'
import Input from './Input'
import Spinner from './Spinner'

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()

const handleOnSubmit = (e) => {
e.preventDefault()

signUpEmailPassword(email, password, {
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
})
}

if (isSuccess) {
return <Navigate to="/" replace={true} />
}

const disableForm = isLoading || needsEmailVerification

return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles['logo-wrapper']}>
<img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" />
</div>

{needsEmailVerification ? (
<p className={styles['verification-text']}>
Please check your mailbox and follow the verification link to verify your email.
</p>
) : (
<form onSubmit={handleOnSubmit} className={styles.form}>
<div className={styles['input-group']}>
<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
/>

<button type="submit" disabled={disableForm} className={styles.button}>
{isLoading ? <Spinner size="sm" /> : 'Create account'}
</button>

{isError ? <p className={styles['error-text']}>{error?.message}</p> : null}
</form>
)}
</div>

<p className={styles.text}>
Already have an account?{' '}
<Link to="/sign-in" className={styles.link}>
Sign in
</Link>
</p>
</div>
)
}

export default SignUp

By default, the user must verify his email address before fully signing up. You can change this setting from your Nhost dashboard.

2. Sign-in

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.

For that, we will use the Nhost hook named useSignInEmailPassword 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:

src/components/SignIn.js
import styles from '../styles/components/SignIn.module.css'
import { useState } from 'react'
import { useSignInEmailPassword } from '@nhost/react'
import { Link, Navigate } from 'react-router-dom'
import Input from './Input'
import Spinner from './Spinner'

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={styles.container}>
<div className={styles.card}>
<div className={styles['logo-wrapper']}>
<img src={process.env.PUBLIC_URL + 'logo.svg'} alt="logo" />
</div>

{needsEmailVerification ? (
<p className={styles['verification-text']}>
Please check your mailbox and follow the verification link to verify your email.
</p>
) : (
<form onSubmit={handleOnSubmit} className={styles.form}>
<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
/>

<button type="submit" disabled={disableForm} className={styles.button}>
{isLoading ? <Spinner size="sm" /> : 'Sign in'}
</button>

{isError ? <p className={styles['error-text']}>{error?.message}</p> : null}
</form>
)}
</div>

<p className={styles.text}>
No account yet?{' '}
<Link to="/sign-up" className={styles.link}>
Sign up
</Link>
</p>
</div>
)
}

export default SignIn

3. Sign-out

Finally, to allow the users to sign out from the app, we can use the Nhost useSignOut hook:

src/components/Layout.js
import { useSignOut } from '@nhost/react'

const Layout = () => {
const { signOut } = useSignOut()

const menuItems = [
//..
{
label: 'Logout',
onClick: signOut,
icon: LogoutIcon
}
]

//...
}

Protect routes

Now that we have implemented 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 using the Nhost SDK:

src/components/ProtectedRoute.js
import styles from '../styles/components/ProtectedRoute.module.css'
import { useAuthenticationStatus } from '@nhost/react'
import { Navigate, useLocation } from 'react-router-dom'
import Spinner from './Spinner'

const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading } = useAuthenticationStatus()
const location = useLocation()

if (isLoading) {
return (
<div className={styles.container}>
<Spinner />
</div>
)
}

if (!isAuthenticated) {
return <Navigate to="/sign-in" state={{ from: location }} replace />
}

return children
}

export default ProtectedRoute

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 (
<NhostProvider nhost={nhost}>
<BrowserRouter>
<Routes>
<Route path="sign-up" element={<SignUp />} />
<Route path="sign-in" element={<SignIn />} />
<Route
path="/"
// highlight-start
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
// highlight-end
>
<Route index element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
</BrowserRouter>
</NhostProvider>
)
}

Retrieve user data

Finally, let's display the information of the authenticated user throughout his dashboard to make the app more personalized.

Getting the current authenticated user data is quite easy. Indeed, we can use the useUserData hook provided by Nhost to do it.

So, open the components/Layout.js file and use this hook like so:

import { useUserData } from '@nhost/react'

const Layout = () => {
const user = useUserData()
//...
}

That's it! The JSX code for rendering the user data (email, display name, etc.) is already included in your components as part of the template you've bootstraped at the beginning of this guide.

Update user data

Nhost provides a GraphQL API through Hasura so that we can query and mutate our data instantly.

In this tutorial, we'll use the Apollo GraphQL client for interacting with this GraphQL API.

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.

src/App.js
import { NhostApolloProvider } from '@nhost/react-apollo'

function App() {
return (
<NhostProvider nhost={nhost}>
<NhostApolloProvider nhost={nhost}>{/* ... */}</NhostApolloProvider>
</NhostProvider>
)
}

From there, we can construct our GraphQL query and use the Apollo useMutation hook to execute that query when the user submits the form from the profile page:

src/pages/Profile.js
import { gql, useMutation } from '@apollo/client'
import { toast } from 'react-hot-toast'

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()

try {
await mutateUser({
variables: {
id: user.id,
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
}
})
toast.success('Updated successfully', { id: 'updateProfile' })
} catch (error) {
toast.error('Unable to update profile', { id: 'updateProfile' })
}
}

//...
}

Finally, since Hasura has an allow nothing by default policy, and we haven't set any permissions yet, our GraphQL mutations would fail.

So, open the Hasura console from the Data tab of your project from your Nhost dashboard. Then, go to the permissions tab of the users table, type in user in the role cell, and click the edit icon on the select operation:

Hasura users permissions

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 each requests.

Hasura users permissions

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

Hasura users permissions

Repeat the same steps on the update operation for the user role to allow users to update their displayName and metadata only.

Finally, to add caching, synchronizing, and updating server state in your React app, let's refactor the user data fetching using the Apollo client and our GraphQL API instead.

So, first add the following GraphQL query to retrieve the current user data from the Layout component:

src/components/Layout.js
import { gql } from '@apollo/client'

const GET_USER_QUERY = gql`
query GetUser($id: uuid!) {
user(id: $id) {
id
email
displayName
metadata
avatarUrl
}
}
`

const Layout = () => {
//...
}

Then, replace the useUserData hook with the useUserId hook to retrieve the current user's ID.

src/components/Layout.js
import { useUserId } from '@nhost/react'

const Layout = () => {
const id = useUserId()
//...
}

Finally, we can run our GraphQL query using the useQuery hook and the current user's ID.

src/components/Layout.js
// highlight-next-line
import { gql, useQuery } from '@apollo/client'

const Layout = () => {
const id = useUserId()
// highlight-start
const { loading, error, data } = useQuery(GET_USER_QUERY, {
variables: { id },
skip: !id
})
const user = data?.user
// highlight-end

//...

return (
<div>
<header>{/* ... */}</header>

<main className={styles.main}>
<div className={styles['main-container']}>
{/* highlight-start */}
{error ? (
<p>Something went wrong. Try to refresh the page.</p>
) : !loading ? (
<Outlet context={{ user }} />
) : null}
{/* highlight-end */}
</div>
</main>
</div>
)
}

You now have a fully functional React application. Congratulations!

Next Steps

  • Did you enjoy Nhost? Give us a star ⭐ on Github. Thank you!
  • Check out our more in-depth examples.
  • Build your next app with Nhost!