Back

Guide


Aug 03, 2022

Guide


Aug 03, 2022

Building a Custom User Profile with Clerk

Subha Chanda

Subha Chanda


Authentication is one of the most critical functions of securing your applications; however, it's also one of the most challenging functions to implement.


Failing to implement a proper authentication system can make your application vulnerable and your users' information open to potential hackers.

If you don't want to get into the technicalities of implementing an authentication system from scratch, Clerk is your solution. Clerk provides built-in components for your application that can be easily integrated into your frontend application, and it supports most of the popular frameworks, like React, Next.js, and Gatsby.

Clerk can be beneficial for your customer identity management. You can easily manage your user sessions, devices, and profiles from the Clerk dashboard. Moreover, you can easily integrate Clerk with services like Firebase or Google Analytics.

In addition, Clerk makes integrating authentication to your existing application simple since it provides built-in hooks for adding authentication. You can implement social sign-in, password-based authentication, or Web3 logins easily using Clerk. You can also extend your functionalities and create custom components and user profiles using their SDK and APIs.

In this article, you'll see how Clerk authentication can be implemented in your Next.js application and how you can implement the Clerk built-in components for user profiles. You'll also learn how to create custom user profiles for your users and update users' profiles using your custom components.

What Is Clerk?

Clerk is an all-in-one solution for your authentication and user management needs. It provides features like password-based or social sign-in, passwordless login using magic links or email, and SMS passcodes. It also provides a user management dashboard, user analytics, allow/block list, rate limiting, and more.

With Clerk, you can set up a complete user management system within ten minutes. Clerk can seamlessly integrate with popular technologies, including React, Next.js, Gatsby, RedwoodJS, Remix, Node.js, and Go.

For popular frontend frameworks, like React, Remix, RedwoodJS, or Gatsby, Clerk has created well-designed components for login, sign-up, the user profile, and the user button. However, it's not limited to built-in components. You can always create custom components with the help of hooks provided by the Clerk SDK.

Because Clerk provides you with frontend SDK, API, and backend APIs for developing custom pages for your users, building a customer user profile is easy.

Building a Custom User Profile Using Clerk

This article aims to show how you can create custom user profiles for your Next.js applications and how to implement the built-in components. To do this, you'll use the Clerk SDK for Next.js.

The basic flow of the application is shown in the following GIF:

Clerk Next.js application walkthrough courtesy of Subha Chanda

Prerequisites

Before you build your custom user profiles with Clerk, you need to satisfy a few prerequisites, including the following:

  • Create a Clerk account.
  • Have a basic understanding of Next.js. To learn more about Next.js, you can check out their documentation.
  • Have a basic understanding of Tailwind CSS (optional)

You can create a free account for Clerk from the Clerk sign-up page. After creating your account, you'll be redirected to the Clerk dashboard. There, you will be asked to create a new application. Add an application with the name of your choice. This is what the setup for this tutorial looks like:

Clerk app creation page

After successfully creating the application, you'll be able to visit the application's dashboard:

Clerk application dashboard

You can find your API keys in the menu under the Developers section. Click on API Keys and copy the frontend API key. You'll need this later in the project.

In addition to your Clerk account, Node.js and npm must be installed on your local computer to create and run the application.

If you want to copy the code and follow along, you can use this GitHub repo.

Creating a Next.js Application

The first step in building this application is to create a Next.js application. To scaffold a Next.js application, run the following command in the terminal:

1
npx create-next-app@latest

Once the installation is complete, you can run the application using npm run dev in the terminal, and the application will start on port 3000. Next.js will render the index.jsx file inside the pages folder. You can delete the contents of the index.jsx file as it will be customized.

The second step is to integrate Tailwind CSS with your Next.js application. You can follow this Tailwind CSS installation guide to do so.

Then install the Clerk SDK for the Next.js plug-in. To install the SDK, run the following command in the terminal:

1
npm install @clerk/nextjs

For using forms effortlessly, you can also integrate the react-hook-form plug-in by running the following command:

1
npm install react-hook-form

Another plug-in called react-icons is used for adding icons to the application. You can install react-icons by simply running the following command in your terminal:

1
npm install react-icons

These previous plug-ins are the only ones that will be used in this application.

Integrating User Profile in Your Next.js App

To add a user profile to your Next.js app using Clerk, you need to obtain the frontend API key, create a new .env.local file, and paste the frontend API key to a variable called NEXT_PUBLIC_CLERK_FRONTEND_API. The .env.local file should look similar to this:

1
NEXT_PUBLIC_CLERK_FRONTEND_API=clerk.noice.bjsdn-81.lcl.dev

Then you need to wrap your Next.js application with the ClerkProvider component. The ClerkProvider wrapper can be found in the @clerk/nextjs SDK. The SDK also has methods called SignedIn, SignedOut, and RedirectToSignIn. These components can be used to secure your application. For example, the components wrapped inside the SignedIn will require the user to sign in. You can read about the different components in the official docs.

The _app.js file for this application should look like this:

1
import '../styles/globals.css';
2
import {
3
ClerkProvider,
4
SignedIn,
5
SignedOut,
6
RedirectToSignIn,
7
} from '@clerk/nextjs';
8
9
import { useRouter } from 'next/router';
10
11
import Header from '../components/Header';
12
13
const publicPages = [];
14
15
function MyApp({ Component, pageProps }) {
16
const { pathname } = useRouter();
17
const isPublicPage = publicPages.includes(pathname);
18
19
return (
20
<ClerkProvider frontendApi={process.env.NEXT_PUBLIC_CLERK_FRONTEND_API}>
21
{isPublicPage ? (
22
<Component {...pageProps} />
23
) : (
24
<>
25
<SignedIn>
26
<Header />
27
<Component {...pageProps} />
28
</SignedIn>
29
<SignedOut>
30
<RedirectToSignIn />
31
</SignedOut>
32
</>
33
)}
34
</ClerkProvider>
35
);
36
}
37
38
export default MyApp;

You must pass the API key in the ClerkProvider through the frontendApi prop. The publicPages array can store the pages that are available to everyone. But for this article, all the pages will be secured.

The SignedIn wrapper holds all the components. That means, if you are signed in, you'll only be able to access the pages. You'll be redirected to the sign-in page if you are not signed in. You'll be redirected to the login screen if you are not logged in using the RedirectToSignIn component.

Header and User Profile

The Header component is a very basic header. The code for the Header component looks like this in the Header.jsx file:

1
import { GiAstronautHelmet } from 'react-icons/gi';
2
import { CgProfile } from 'react-icons/cg';
3
import { UserButton } from '@clerk/clerk-react';
4
5
import Link from 'next/link';
6
7
const Header = (props) => {
8
return (
9
<div className='w-full bg-purple-600 py-4'>
10
<div className='w-10/12 mx-auto flex justify-between items-center'>
11
<Link href={'/'}>
12
<a>
13
<h4 className='text-white text-2xl font-bold flex items-center'>
14
<GiAstronautHelmet className='mr-4' />
15
Clerk is Awesome
16
</h4>
17
</a>
18
</Link>
19
<div>
20
<UserButton userProfileUrl='/profile' />
21
</div>
22
</div>
23
</div>
24
);
25
};
26
27
export default Header;

You can see that the Header component is using a component called UserButton. Clerk provides this component, and the button renders a toggle menu for accessing the user profile:

`UserButton` demo

The userProfileUrl prop holds the location of the Manage account page. Here, the location is /profile.

Now, create a new page called profile.jsx inside the pages folder. You only need to render a single component to generate the user profile:

1
import { UserProfile } from '@clerk/nextjs';
2
3
const Profile = () => {
4
return <UserProfile />;
5
};
6
export default Profile;

The UserProfile component provided by Clerk does everything for you and generates an aesthetically pleasing user profile.

If you try to visit the /profile route now, you'll need to sign in or create a new account. Clerk already handles the authorization for accessing specific pages. After successfully logging in, you'll be able to visit the user profile page:

Clerk `UserProfile` component

You can also update your profile from here, which you'll learn about in the next section.

Implementing Custom User Profile

To implement a custom user profile, create a new page called view.jsx inside your pages directory. This page will render a custom user profile. As this article focuses more on the technicalities of implementing a custom user profile, the design and Tailwind classes will not be discussed.

To help the user access the custom user profile, update your index.jsx file like this:

1
import Head from 'next/head';
2
import styles from '../styles/Home.module.css';
3
4
import Link from 'next/dist/client/link';
5
6
const Home = () => {
7
return (
8
<div className={styles.container}>
9
<Head>
10
<title>Create Next App</title>
11
<meta name='description' content='Generated by create next app' />
12
<link rel='icon' href='/favicon.ico' />
13
</Head>
14
<div className='w-full h-screen flex items-center justify-center flex-col'>
15
<h1 className='text-6xl text-purple-600 font-bold'>
16
Clerk is Awesome
17
</h1>
18
<Link href={'/view'}>
19
<a>
20
<button className='bg-purple-600 text-white font-bold py-2 px-4 mt-4 hover:bg-purple-800 transition-all'>
21
View Profile
22
</button>
23
</a>
24
</Link>
25
</div>
26
<div></div>
27
</div>
28
);
29
};
30
31
export default Home;

The previous code shows a centered text, "Clerk is Awesome," and a button for accessing the custom user profile with a link of /view.

The final design of the custom profile page will look like this:

Custom user profile screen

You'll have to use the useUser hook available in the Clerk SDK to get the necessary information from the backend. The useUser hook returns the values of the current user along with other important information, like the user creation date and external connected accounts if two-factor authentication is connected:

`useUser` returned data

If you look at the returned data closely, you'll find that the data contains an object unsafeMetadata. The unsafeMetadata object can hold custom values stored for custom user profile information:

`unsafeMetadata` object

For this article, you can see there are two custom fields: customBio and customName.

Clerk has three types of metadata for storing additional user information: public, private, and unsafe. Both public and private metadata can be updated or added from the backend, and you can access or view only the public metadata from the frontend. The unsafe metadata can be read or written from the frontend:

Comparison of metadata types

Because of the ability to write from the frontend, this article will use unsafe metadata for custom user information. You can read more about metadata in Clerk's documentation.

Look at the view.jsx file:

1
import { useUser } from '@clerk/nextjs';
2
3
import Image from 'next/image';
4
5
import Link from 'next/link';
6
7
const ViewProfile = () => {
8
const { isLoaded, isSignedIn, user } = useUser();
9
if (!isLoaded || !isSignedIn) {
10
return null;
11
}
12
13
console.log(user);
14
15
return (
16
<div className='container mx-auto'>
17
<div className='flex'>
18
<div className='mx-4'>
19
<Image
20
src={user.profileImageUrl}
21
width={'250px'}
22
height={'250px'}
23
alt={user.fullName}
24
className='rounded-lg'
25
/>
26
</div>
27
<div className='ml-4'>
28
<div className='-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto'>
29
<div className='inline-block w-full shadow-md rounded-lg overflow-hidden'>
30
<table className='w-full leading-normal'>
31
<tbody>
32
{/* Firstname */}
33
<tr>
34
<td className='px-5 py-5 border-b border-gray-200 bg-white text-sm text-gray-900 whitespace-no-wrap'>
35
First Name
36
</td>
37
<td className='px-5 py-5 border-b border-gray-200 bg-white text-sm text-gray-900 whitespace-no-wrap'>
38
{user.firstName}
39
</td>
40
</tr>
41
{/* Last Name */}
42
<tr>
43
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
44
Last Name
45
</td>
46
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
47
{user.lastName}
48
</td>
49
</tr>
50
{/* Emails */}
51
<tr>
52
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
53
Emails
54
</td>
55
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
56
{user.emailAddresses.map((email) => (
57
<div key={email.emailAddress}>
58
{email.emailAddress},{' '}
59
</div>
60
))}
61
</td>
62
</tr>
63
{/* Unsafe Metadata Example */}
64
<tr>
65
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
66
Custom Name
67
</td>
68
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
69
{user.unsafeMetadata.customName}
70
</td>
71
</tr>
72
<tr>
73
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
74
Custom Bio
75
</td>
76
<td className='text-gray-900 whitespace-no-wrap px-5 py-5 border-b border-gray-200 bg-white text-sm'>
77
{user.unsafeMetadata.customBio}
78
</td>
79
</tr>
80
</tbody>
81
</table>
82
</div>
83
</div>
84
<div className='flex justify-center'>
85
<Link href={'/additional'}>
86
<button className='bg-purple-600 text-white font-bold py-2 px-4 mt-4 hover:bg-purple-800 transition-all'>
87
Update Additional Information
88
</button>
89
</Link>
90
</div>
91
</div>
92
</div>
93
</div>
94
);
95
};
96
97
export default ViewProfile;

Don't get overwhelmed. The code can look complicated, but it's not. Now, break down the essential components. Begin by importing the useUser hook from the Clerk SDK. Then the Image and Link components are imported from Next.js.

The ViewProfile component renders the user profile. The initial step of using useUser is to destructure its essential functions:

1
const { isLoaded, isSignedIn, user } = useUser();
2
if (!isLoaded || !isSignedIn) {
3
return null;
4
}

In the previous code, the functions check if the page is not loaded or the user is not signed in. If not, then nothing is rendered. You can console the user object here:

1
console.log(user);

This will return all the information available to Clerk for the particular user. Now that you have access to the user object, you can use it to render the profile values. For example, you can generate the user's profile image by simply accessing the user.profileImageUrl key. The first name of the user is stored inside the user.firstName key.

The template here only uses these keys: user.profileImageUrl, user.firstName, user.lastName, user.fullName, user.emailAddresses, and user.unsafeMetadata. The custom user profile can be implemented using the user object.

Updating Current User Profile

If you look at the previous code, you'll find that it also contains a link to another page for updating the profile information. Look at how you can edit user information from a custom profile update page.

Create a new page with the name additional.jsx file inside the pages directory. The react-hook-form plug-in will be used here, though it's not necessary for a simple form like this. If your form is large and complex, react-hook-form is a great solution. This plug-in makes the binding of the input fields simple. You can look at React Hook Form's "Get Started" example to get a basic idea of how it works.

Now take a look at the complete code and then break it into parts:

1
import { useForm } from 'react-hook-form';
2
import { useUser } from '@clerk/nextjs/dist/client';
3
4
import { useRouter } from 'next/router';
5
6
const AdditionalUpdate = () => {
7
const router = useRouter();
8
9
const {
10
register,
11
handleSubmit,
12
watch,
13
formState: { errors },
14
} = useForm();
15
16
const { isLoaded, isSignedIn, user } = useUser();
17
18
const onSubmit = (data) => {
19
try {
20
user.update({
21
firstName: data.firstName,
22
lastName: data.lastName,
23
unsafeMetadata: {
24
customName: data.customName,
25
customBio: data.customBio,
26
},
27
});
28
29
router.push('/view');
30
} catch (error) {
31
console.log(error);
32
}
33
};
34
35
if (!isLoaded || !isSignedIn) {
36
return null;
37
}
38
39
return (
40
<div className='mx-10'>
41
<h1 className='text-2xl font-bold py-4'>Update Additional Information</h1>
42
<form onSubmit={handleSubmit(onSubmit)}>
43
<div>
44
<label
45
className='block text-gray-700 text-sm font-bold mb-2'
46
htmlFor='firstName'
47
>
48
First Name
49
</label>
50
<input
51
defaultValue={user.firstName}
52
{...register('firstName', {
53
required: true,
54
})}
55
className='shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'
56
/>
57
{errors.firstName && (
58
<span className='text-sm text-red-600'>This field is required</span>
59
)}
60
</div>
61
<div>
62
<label
63
className='block text-gray-700 text-sm font-bold mb-2'
64
htmlFor='lastName'
65
>
66
Last Name
67
</label>
68
<input
69
defaultValue={user.lastName}
70
{...register('lastName', {
71
required: true,
72
})}
73
className='shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'
74
/>
75
{errors.lastName && (
76
<span className='text-sm text-red-600'>This field is required</span>
77
)}
78
</div>
79
<div>
80
<label
81
className='block text-gray-700 text-sm font-bold mb-2'
82
htmlFor='customName'
83
>
84
Custom Name
85
</label>
86
<input
87
defaultValue={user.unsafeMetadata.customName}
88
{...register('customName', {
89
required: true,
90
})}
91
className='shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'
92
/>
93
{errors.customName && (
94
<span className='text-sm text-red-600'>This field is required</span>
95
)}
96
</div>
97
<div className='mt-4'>
98
<label
99
className='block text-gray-700 text-sm font-bold mb-2'
100
htmlFor='customBio'
101
>
102
Custom Bio
103
</label>
104
<textarea
105
rows={6}
106
defaultValue={user.unsafeMetadata.customBio}
107
{...register('customBio', {
108
required: true,
109
})}
110
className='shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'
111
></textarea>
112
{errors.customBio && (
113
<span className='text-sm text-red-600'>This field is required</span>
114
)}
115
</div>
116
117
<button
118
type='submit'
119
className='bg-purple-500 px-8 py-2 my-4 text-lg font-semibold text-white hover:bg-purple-700 transition-all'
120
>
121
Update
122
</button>
123
</form>
124
</div>
125
);
126
};
127
128
export default AdditionalUpdate;

The onSubmit function is used for saving the updated information to the server. The user.update function is used for updating the values. A new object with the updated values is passed into this function:

1
user.update({
2
firstName: data.firstName,
3
lastName: data.lastName,
4
unsafeMetadata: {
5
customName: data.customName,
6
customBio: data.customBio,
7
},
8
});

As you can see from the previous object, the firstName, lastName, and two custom fields are being updated. The custom fields can be updated by updating the keys inside unsafeMetadata.

The user.update function is wrapped inside a try…catch block. The page will be redirected to the custom user profile if the object is successfully updated.

But how do you render the already existing values of the user? It's implemented using a similar approach to building a custom user profile. The defaultValue of the input field is filled with the corresponding user object value:

1
<input
2
defaultValue={user.firstName}
3
{...register('firstName', {
4
required: true,
5
})}
6
/>

The register method is a react-hook-form method that registers the input field with the value passed. For example, the previous code registers the value with firstName. You can access this value by accessing the data.firstName object.

Finally, the complete template is placed inside form tags, where the onSubmit function looks like this: onSubmit={handleSubmit(onSubmit)}. The handleSubmit function is a react-hook-form function that handles submissions. It takes in another function as a parameter, and the onSubmit function is passed here.

The last thing you need to do is add www.gravatar.com to your next.config.js file. When there is no profile picture set by the user for their profile, a Gravatar is shown. The image component in Next.js requires the hostnames to be added to the next.config.js. Your next.config.js file should look like this:

1
/** @type {import('next').NextConfig} */
2
const nextConfig = {
3
reactStrictMode: true,
4
swcMinify: true,
5
images: {
6
domains: ['images.clerk.com', 'www.gravatar.com'],
7
},
8
};
9
10
module.exports = nextConfig;

Your user profile update page is now ready.

You can access and check the page by visiting the localhost URL, http://localhost:3000/additional. You can also check this GitHub repo for all the code from this tutorial.

The functionalities discussed earlier can also be implemented using the Clerk frontend API. The frontend API has endpoints like https://clerk.example.com/v1/me for updating the user profile from the frontend. You can check the frontend API documentation to learn more.

Conclusion

Clerk is a great solution for quickly integrating authentication and custom user profiles into your application. It provides more than authentication; you can manage users, sessions, APIs, and more right from the Clerk dashboard.

This article aims to show you how custom user profiles can be built using the Next.js Clerk SDK. You also saw how Clerk components could be used for rapid development.

You can get started with Clerk for free for up to 500 monthly active users in your application. To create a Clerk account, visit their sign-up page.

Clerk's logo

Start now,
no strings attached

Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.

Start Building

Pricing built for
businesses of all sizes.

Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.

Clerk's logo

Newsletter!

The latest news and updates from Clerk, sent to your inbox.