Quickstart: RedwoodJS
Introduction
This quickstart guide provides the steps you need to build a simple RedwoodJS app powered by Nhost for the backend. It includes:
- Database: PostgreSQL
- Instant GraphQL API: Hasura
- Authentication: Hasura Auth
- Storage: Hasura Storage
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 >=14.19 <=16.x: install it from here.
- You'll need Yarn >=1.15: install it from here.
To check, please run the following commands from your terminal:
node --version
yarn --version
If your system versions do not meet both requirements, the RedwoodJS installation will result in an error. So, make sure you have the correct versions before moving on.
Project setup
Create a new Nhost app
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.
Next, give your new Nhost project a name, select a geographic region for your Nhost services and click Create Project.
After a few seconds, you should get a PostgreSQL database, a GraphQL API with Hasura, file storage, and authentication set up.
Finally, update your client login URL from your Nhost dashboard as the local RedwoodJS server starts on port 8910
instead of the usual 3000
port you may be used to with a regular React app.
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 RedwoodJS app
The simplest way to create a new RedwoodJS application is by using the yarn create
command, which bootstraps a RedwoodJS app for you without the hassle of configuring everything yourself.
So, open your terminal, and run the following command:
- Javascript
- Typescript
yarn create redwood-app my-nhost-app
yarn create redwood-app my-nhost-app --typescript
You can now cd
into your project directory:
cd my-nhost-app
And run the development server with the following command:
yarn rw dev
If everything is working fine, your RedwoodJS development server should be running on port 8910. Open http://localhost:8910 from your browser to check this out.
Configure Nhost with RedwoodJS
To work with Nhost from within our RedwoodJS app, we'll use the JavaScript SDK provided by Nhost which allows us to interact with our Nhost backend using a standard interface.
You can install the Nhost JavaScript SDK with the following CLI command:
yarn rw setup auth nhost
When prompted "Overwrite existing /api/src/lib/auth.[jt]s?", reply yes.
In addition to installing the Nhost JavaScript SDK, this command configures and instantiates Nhost as the authentication client in our RedwoodJS app. It makes the authentication state and all the provided hooks used with Nhost Authentication available in our application through the <AuthProvider>
.
import { AuthProvider } from '@redwoodjs/auth'
// highlight-next-line
import { NhostClient } from '@nhost/nhost-js'
// highlight-start
const nhostClient = new NhostClient({
subdomain: process.env.NHOST_SUBDOMAIN
region: process.env.NHOST_REGION
})
// highlight-end
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
{/* highlight-next-line */}
<AuthProvider client={nhostClient} type="nhost">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
export default App
Next, as we are also using Nhost as our GraphQL API server instead of the standard RedwoodJS GraphQL Server, we need to pass skipFetchCurrentUser
as a prop to AuthProvider
, as follows:
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
{/* highlight-next-line */}
<AuthProvider client={nhostClient} type="nhost" skipFetchCurrentUser>
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
That prop avoids having an additional request to fetch the current user.
Next, store the environment variables for subdomain
and region
in .env
:
NHOST_SUBDOMAIN=[subdomain]
NHOST_REGION=[region]
You find your Nhost project's subdomain
and region
in the project overview:
Finally, we need to customize the GraphQL Endpoint for our RedwoodJS app. Indeed, by default, RedwoodJS provides a built-in GraphQL server under the api
side. However, as we already have our own GraphQL server through Nhost and Hasura, we do not need it.
So, open the RedwoodJS configuration file, redwood.toml
, and change the GraphQL endpoint via the apiGraphQLUrl
option, as follows:
[web]
apiUrl = "/.redwood/functions"
# highlight-next-line
apiGraphQLUrl = "https://${NHOST_SUBDOMAIN}.graphql.${NHOST_REGION}.nhost.run/v1"
Don't forget to restart your RedwoodJS server after saving your .env
and redwood.toml
files to load your new environment variable and configuration.
Setup a styling library
For the purpose of this guide, we'll set up Tailwind CSS as our styling library inside our project as the installation process is straightforward and supported by default by RedwoodJS.
Indeed, you only need to run the following command:
yarn rw setup ui tailwindcss
RedwoodJS will take care of installing the required dependencies, configuring PostCSS, initializing Tailwind CSS, and adding the necessary imports.
You may need to restart your RedwoodJS server after setting up Tailwind CSS for your configuration to take effect.
Finally, add the following extra custom CSS classes to your index.css
file so we can re-use them throughout our app to style some of the repeated HTML elements (input
and button
):
/**
* START --- SETUP TAILWINDCSS EDIT
*
* `yarn rw setup ui tailwindcss` placed these imports here
* to inject Tailwind's styles into your CSS.
* For more information, see: https://tailwindcss.com/docs/installation#include-tailwind-in-your-css
*/
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
/**
* END --- SETUP TAILWINDCSS EDIT
*/
@layer components {
.input {
@apply w-full shadow-sm rounded-md p-3 border border-gray-300 transition;
}
.input:focus {
@apply outline-none border-blue-500 ring-blue-500 ring-4 ring-opacity-20;
}
.input:disabled {
@apply opacity-50 cursor-not-allowed;
}
.input:read-only {
@apply opacity-50 cursor-not-allowed;
}
.input:read-only:focus {
@apply border-gray-300 focus:ring-0;
}
.label {
@apply text-gray-700 font-medium text-sm mb-1;
}
.btn-primary {
@apply w-full font-medium inline-flex justify-center items-center rounded-md p-3 text-white bg-blue-600 transition;
}
.btn-primary:enabled:hover {
@apply bg-blue-500;
}
.btn-primary:focus {
@apply outline-none ring-4 ring-blue-500 ring-opacity-50;
}
.btn-primary:disabled {
@apply opacity-50 cursor-not-allowed;
}
.btn-secondary {
@apply py-2 px-4 rounded-md bg-gray-700 text-white transition;
}
.btn-secondary:enabled:hover {
@apply bg-gray-600;
}
.btn-secondary:focus {
@apply outline-none ring-4 ring-gray-700 ring-opacity-20;
}
.btn-secondary:disabled {
@apply opacity-50 cursor-not-allowed;
}
}
Check the official documentation to see all the UI libraries supported by the setup ui
command.
Build the app
Create the pages
Let's start by creating the pages of our project using the redwood
CLI:
yarn rw generate page home /
yarn rw generate page profile /profile
yarn rw generate page SignUp /sign-up
yarn rw generate page SignIn /sign-in
Each command will create a new page component under the web/pages
folder and add a <Route>
in web/src/Routes.js
that maps the path to the new page.
import { Router, Route } from '@redwoodjs/router'
const Routes = () => {
return (
<Router>
{/* highlight-start */}
<Route path="/" page={HomePage} name="home" />
<Route path="/profile" page={ProfilePage} name="profile" />
<Route path="/sign-in" page={SignInPage} name="signIn" />
<Route path="/sign-up" page={SignUpPage} name="signUp" />
{/* highlight-end */}
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
Your pages should be up and running already with some default content. For example, if you visit your homepage at http://localhost:8910, here's what you should see:
Create the layout
As both our HomePage
and ProfilePage
components will have the same layout, we can wrap them inside a single layout component that defines the shared UI and then renders the page as its child. That saves us from writing duplicated code.
So, let's create that layout by running the following command:
yarn redwood g layout dash
That command creates the layout component in the web/src/layouts/DashLayout/DashLayout.js
file. So, open up this file and use the following code to define the UI of the layout:
import { Fragment } from 'react'
import { Link, navigate, routes } from '@redwoodjs/router'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon, HomeIcon, LogoutIcon, UserIcon } from '@heroicons/react/outline'
const Avatar = ({ src = '', alt = '' }) => {
return (
<div className="rounded-full bg-gray-100 overflow-hidden w-9 h-9">
{src ? <img src={src} alt={alt} /> : null}
</div>
)
}
const DashLayout = ({ children }) => {
const user = null
const menuItems = [
{
label: 'Dashboard',
onClick: () => navigate(routes.home()),
icon: HomeIcon
},
{
label: 'Profile',
onClick: () => navigate(routes.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={routes.home()} className="h-9">
<img src="https://i.imgur.com/CmLlufz.png" alt="logo" className="max-h-full w-auto" />
</Link>
<Menu as="div" className="relative z-50">
<Menu.Button className="flex items-center space-x-px group">
<Avatar src={user?.avatarUrl} alt={user?.displayName} />
<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={user?.avatarUrl} alt={user?.displayName} />
</div>
<div className="flex flex-col truncate">
<span>{user?.displayName}</span>
<span className="text-sm text-gray-500">{user?.email}</span>
</div>
</div>
<div className="py-2">
{menuItems.map(({ label, onClick, icon: Icon }) => (
<div key={label} className="px-2 last:border-t last:pt-2 last:mt-2">
<Menu.Item>
<button
onClick={onClick}
className="w-full 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>
</button>
</Menu.Item>
</div>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</header>
<main className="mt-[60px]">
<div className="container mx-auto px-4 py-12">{children}</div>
</main>
</div>
)
}
export default DashLayout
Make sure to install the following third-party dependencies as we are using them to create the dropdown menu and render SVG icons:
yarn workspace web add @headlessui/react @heroicons/react
Finally, we need to edit the Routes.js
file to render our layout.
So, import the DashLayout
component at the top and wrap the /
and /profile
routes inside it using the Set
component from @redwoodjs/router
:
// highlight-start
import { Router, Route, Set } from '@redwoodjs/router'
import DashLayout from 'src/layouts/DashLayout'
// highlight-end
const Routes = () => {
return (
<Router>
{/* highlight-start */}
<Set wrap={DashLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/profile" page={ProfilePage} name="profile" />
</Set>
{/* highlight-end */}
<Route path="/sign-in" page={SignInPage} name="signIn" />
<Route path="/sign-up" page={SignUpPage} name="signUp" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
Your home and profile pages should now look something like this:
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 useAuth
hook provided by RedwoodJS (which leverages the Nhost JavaScript SDK under the hood) within our sign-up page.
So, open up the corresponding file from your project, and use the following code:
import { MetaTags } from '@redwoodjs/web'
import { useState } from 'react'
import { Link, Redirect, routes, useParams } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { Form, Label, EmailField, PasswordField, TextField, Submit } from '@redwoodjs/forms'
const SignUpPage = () => {
const { isAuthenticated, signUp } = useAuth()
const { redirectTo } = useParams()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [needsEmailVerification, setNeedsEmailVerification] = useState(false)
const handleOnSubmit = async ({ email, password, firstName, lastName }) => {
try {
setLoading(true)
const { session, error } = await signUp({
email,
password,
options: {
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
}
})
if (error) {
throw error
}
if (!session) {
setNeedsEmailVerification(true)
setLoading(false)
}
} catch (error) {
setError(error.message)
setLoading(false)
}
}
if (isAuthenticated) {
return <Redirect to={redirectTo ?? routes.home()} options={{ replace: true }} />
}
return (
<>
<MetaTags title="Sign Up" description="SignUp page" />
<div className="h-screen flex items-center justify-center py-6">
<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="https://i.imgur.com/CmLlufz.png" alt="logo" className="w-full h-full" />
</div>
{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="mt-12 flex flex-col items-center space-y-6">
<div className="w-full flex gap-6">
<div className="w-full flex flex-col">
<Label name="firstName" className="label">
First name
</Label>
<TextField name="firstName" required className="input" disabled={loading} />
</div>
<div className="w-full flex flex-col">
<Label name="lastName" className="label">
Last name
</Label>
<TextField name="lastName" required className="input" disabled={loading} />
</div>
</div>
<div className="w-full flex flex-col">
<Label name="email" className="label">
Email address
</Label>
<EmailField name="email" required className="input" disabled={loading} />
</div>
<div className="w-full flex flex-col">
<Label name="password" className="label">
Password
</Label>
<PasswordField name="password" required className="input" disabled={loading} />
</div>
</div>
<Submit className="mt-6 btn-primary" disabled={loading}>
Create account
</Submit>
{error ? <p className="mt-4 text-red-500 text-center">{error}</p> : null}
</Form>
)}
</div>
{!needsEmailVerification ? (
<p className="sm:mt-8 text-gray-500 text-center">
Already have an account?{' '}
<Link
to={routes.signIn()}
className="text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition"
>
Sign in
</Link>
</p>
) : null}
</div>
</div>
</>
)
}
export default SignUpPage
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 also use the useAuth
hook from RedwoodJS inside our
sign-in page the same way we did with our sign-up page. So, here's
what your component should look like after applying the changes for the sign-in
logic:
import { MetaTags } from '@redwoodjs/web'
import { useState } from 'react'
import { Link, Redirect, routes, useParams } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { Form, Label, EmailField, PasswordField, Submit } from '@redwoodjs/forms'
const SignInPage = () => {
const { isAuthenticated, logIn } = useAuth()
const { redirectTo } = useParams()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [needsEmailVerification, setNeedsEmailVerification] = useState(false)
const handleOnSubmit = async ({ email, password }) => {
try {
setLoading(true)
const { error } = await logIn({
email,
password
})
if (error) {
if (error?.error === 'unverified-user') {
setNeedsEmailVerification(true)
setLoading(false)
} else {
throw error
}
}
} catch (error) {
setError(error.message)
setLoading(false)
}
}
if (isAuthenticated) {
return <Redirect to={redirectTo ?? routes.home()} options={{ replace: true }} />
}
return (
<>
<MetaTags title="Sign In" description="SignIn page" />
<div className="h-screen flex items-center justify-center py-6">
<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="https://i.imgur.com/CmLlufz.png" alt="logo" className="w-full h-full" />
</div>
{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="mt-12 w-full flex flex-col items-center space-y-6">
<div className="w-full flex flex-col">
<Label name="email" className="label">
Email address
</Label>
<EmailField name="email" required className="input" disabled={loading} />
</div>
<div className="w-full flex flex-col">
<Label name="password" className="label">
Password
</Label>
<PasswordField name="password" required className="input" disabled={loading} />
</div>
</div>
<Submit className="mt-6 btn-primary" disabled={loading}>
Sign in
</Submit>
{error ? <p className="mt-4 text-red-500 text-center">{error}</p> : null}
</Form>
)}
</div>
{!needsEmailVerification ? (
<p className="sm:mt-8 text-gray-500 text-center">
No account yet?{' '}
<Link
to={routes.signUp()}
className="text-blue-600 hover:text-blue-500 hover:underline hover:underline-offset-1 transition"
>
Sign up
</Link>
</p>
) : null}
</div>
</div>
</>
)
}
export default SignInPage
3. Sign-out
Finally, to allow the users to sign out from the app, we can use the RedwoodJS
logOut
method from the useAuth
hook:
// highlight-next-line
import { useAuth } from '@redwoodjs/auth'
const DashLayout = ({ children }) => {
// highlight-next-line
const { logOut } = useAuth()
const menuItems = [
//...
{
label: 'Logout',
// highlight-next-line
onClick: logOut,
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 wrap those two routes inside the Private
component provided by the built-in RedwoodJS router:
// highlight-next-line
import { Private, Router, Route, Set } from '@redwoodjs/router'
import DashLayout from './layouts/DashLayout/DashLayout'
const Routes = () => {
return (
<Router>
// highlight-next-line
<Private unauthenticated="signIn">
<Set wrap={DashLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/profile" page={ProfilePage} name="profile" />
</Set>
// highlight-next-line
</Private>
<Route path="/sign-up" page={SignUpPage} name="signUp" />
<Route path="/sign-in" page={SignInPage} name="signIn" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
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 userMetadata
variable returned by the useAuth
hook.
So, open the files where we need the user's data and retrieve it like so:
- DashLayout
- HomePage
- ProfilePage
const DashLayout = ({ children }) => {
// highlight-next-line
const { logOut, userMetadata: user } = useAuth()
//...
}
import { Link, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
// highlight-next-line
import { useAuth } from '@redwoodjs/auth'
const HomePage = () => {
// highlight-next-line
const { userMetadata: user } = useAuth()
return (
<>
<MetaTags title="Home" description="Home page" />
<h2 className="text-3xl font-semibold">HomePage</h2>
<p className="mt-2 text-lg">
Welcome, {user?.metadata?.firstName || 'stranger'}{' '}
<span role="img" alt="hello">
👋
</span>
</p>
<p className="mt-4 text-gray-500">
Find me in{' '}
<code className="bg-blue-100 text-blue-500 px-2 py-px rounded">
./web/src/pages/HomePage/HomePage.js
</code>
</p>
<p className="mt-4 text-gray-500">
My default route is named{' '}
<code className="bg-blue-100 text-blue-500 px-2 py-px rounded">home</code>, link to me with
`
<Link to={routes.home()} className="text-blue-500 underline">
Home
</Link>
`
</p>
</>
)
}
export default HomePage
import { Form, Label, Submit, EmailField, TextField } from '@redwoodjs/forms'
import { MetaTags } from '@redwoodjs/web'
// highlight-next-line
import { useAuth } from '@redwoodjs/auth'
const ProfilePage = () => {
// highlight-next-line
const { userMetadata: user } = useAuth()
const updateUserProfile = async ({ firstName, lastName }) => null
return (
<>
<MetaTags title="Profile" description="Profile page" />
<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={updateUserProfile}>
<div className="px-4 md:px-8 py-6 space-y-6">
<div className="flex flex-col sm:flex-row gap-6">
<div className="w-full flex flex-col">
<Label name="firstName" className="label">
First name
</Label>
<TextField
type="text"
name="firstName"
required
className="input"
{/* highlight-next-line */}
defaultValue={user?.metadata?.firstName}
/>
</div>
<div className="w-full flex flex-col">
<Label name="lastName" className="label">
Last name
</Label>
<TextField
type="text"
name="lastName"
required
className="input"
{/* highlight-next-line */}
defaultValue={user?.metadata?.lastName}
/>
</div>
</div>
<div className="sm:max-w-md">
<div className="w-full flex flex-col">
<Label name="email" className="label">
Email address
</Label>
<EmailField
name="email"
readOnly
className="input"
{/* highlight-next-line */}
defaultValue={user?.email}
/>
</div>
</div>
</div>
<div className="w-full bg-gray-50 py-4 px-4 md:px-8 flex justify-end">
<Submit className="btn-secondary">
Update
</Submit>
</div>
</Form>
</div>
</div>
</div>
</>
)
}
export default ProfilePage
- Home
- Profile
Update user data
Nhost provides a GraphQL API through Hasura so that we can query and mutate our data instantly.
Also, RedwoodJS comes with the Apollo Client already set up and ready to query thanks to the RedwoodApolloProvider
, which wraps the ApolloProvider
. Hence, we don't need to install or configure any GraphQL client ourselves.
// highlight-next-line
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
// ...
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<AuthProvider client={nhostClient} type="nhost" skipFetchCurrentUser>
{/* highlight-next-line */}
<RedwoodApolloProvider>
<Routes />
{/* highlight-next-line */}
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
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:
import { Form, Label, Submit, EmailField, TextField } from '@redwoodjs/forms'
// highlight-next-line
import { MetaTags, useMutation } from '@redwoodjs/web'
import { useAuth } from '@redwoodjs/auth'
// highlight-start
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
}
}
`
// highlight-end
const ProfilePage = () => {
const { userMetadata: user } = useAuth()
// highlight-next-line
const [mutateUser, { loading }] = useMutation(UPDATE_USER_MUTATION)
const updateUserProfile = async ({ firstName, lastName }) => {
try {
// highlight-start
await mutateUser({
variables: {
id: user.id,
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
}
})
// highlight-end
alert('Updated successfully')
} catch (error) {
alert('Unable to update profile')
}
}
//...
}
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:
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.
Next, select the columns you'd like the users to have access to, and click Save 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 RedwoodJS 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, and use React Context to run that query and share its result throughout your application:
import React, { useContext } from 'react'
import { useAuth } from '@redwoodjs/auth'
import { useQuery } from '@redwoodjs/web'
const GET_USER_QUERY = gql`
query GetUser($id: uuid!) {
user(id: $id) {
id
email
displayName
metadata
avatarUrl
}
}
`
const UserContext = React.createContext(null)
export function UserProvider({ children = null }) {
const { userMetadata } = useAuth()
const { loading, error, data } = useQuery(GET_USER_QUERY, {
variables: { id: userMetadata?.id },
skip: !userMetadata?.id
})
const user = data?.user
if (error) {
return <p>Something went wrong. Try to refresh the page.</p>
}
if (loading) {
return null
}
return <UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
}
export function useUserContext() {
return useContext(UserContext)
}
Then, add the UserProvider
inside your Routes.js
file as follow:
// highlight-next-line
import { UserProvider } from './UserProvider'
const Routes = () => {
return (
<Router>
<Private unauthenticated="signIn">
{/* highlight-next-line */}
<Set wrap={[UserProvider, DashLayout]}>
<Route path="/" page={HomePage} name="home" />
<Route path="/profile" page={ProfilePage} name="profile" />
</Set>
</Private>
<Route path="/sign-up" page={SignUpPage} name="signUp" />
<Route path="/sign-in" page={SignInPage} name="signIn" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
Finally, replace the userMetadata
with the useUserContext
hook to retrieve the current user data inside the following files:
- DashLayout
- HomePage
- ProfilePage
// highlight-next-line
import { useUserContext } from 'src/UserProvider'
const DashLayout = ({ children }) => {
// highlight-next-line
const { user } = useUserContext()
const { logOut } = useAuth()
//...
}
// highlight-next-line
import { useUserContext } from 'src/UserProvider'
const HomePage = () => {
// highlight-next-line
const { user } = useUserContext()
//...
}
// highlight-next-line
import { useUserContext } from 'src/UserProvider'
const ProfilePage = () => {
// highlight-next-line
const { user } = useUserContext()
//...
}
You now have a fully functional RedwoodJS application powered by Nhost on the backend. Congratulations!