Manage member roles in an organization
Organization members with appropriate permissions can manage member roles within an organization, including changing a member's role, removing them from an organization, and revoking organization invitations.
The useOrganization()
hook can be used to retrieve the current user's membership or the list of memberships for the currently active organization. The destroy
and update
methods can be called on the membership object to remove a member or change their role.
The useOrganization()
hook can also be used to retrieve the list of invitations for the currently active organization. The revoke
method can be called on the invitation object to revoke the invitation.
Methods for changing a member's role, removing a member, and revoking an invitation are also available on the Backend API.
Usage
import { useState, useEffect, ChangeEventHandler, useRef } from "react"
import { useOrganization, useUser } from "@clerk/nextjs"
import type { OrganizationCustomRoleKey } from "@clerk/types"
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
export const OrgInvitationsParams = {
invitations: {
pageSize: 5,
keepPreviousData: true,
},
}
export const OrgMembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// View and manage active organization members,
// along with any pending invitations.
// Invite new members.
export default function Organization() {
const {
organization: currentOrganization,
isLoaded,
} = useOrganization();
if (!isLoaded || !currentOrganization) {
return null;
}
return (
<>
<h1>Organization: {currentOrganization.name}</h1>
<h2 className="mb-6 mt-12">Custom List Invitations</h2>
<OrgInvitations />
<h2 className="mb-6 mt-12">Custom List Membership Requests</h2>
<OrgMembershipRequests />
<h2 className="mb-6 mt-12">Custom List Memberships</h2>
<OrgMembers />
<h2 className="mb-6 mt-12">Custom Invite Form</h2>
<OrgInviteMemberForm />
</>
);
}
// List of organization memberships. Administrators can
// change member roles or remove members from the organization.
export const OrgMembers = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Joined</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{memberships?.data?.map((mem) => (
<tr key={mem.id}>
<td>
{mem.publicUserData.identifier}{" "}
{mem.publicUserData.userId === user?.id && "(You)"}
</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>
<SelectRole
defaultRole={mem.role}
onChange={async (e) => {
await mem.update({
role: e.target.value as OrganizationCustomRoleKey,
})
await memberships?.revalidate()
}}
/>
</td>
<td>
<button
onClick={async () => {
await mem.destroy()
await memberships?.revalidate()
}}
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onClick={() => memberships?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onClick={() => memberships?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
// List of pending invitations to an organization.
// You can invite new organization members and
// revoke already sent invitations.
export const OrgInvitations = () => {
const { isLoaded, invitations, memberships } = useOrganization({
...OrgInvitationsParams,
...OrgMembersParams,
})
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Invited</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{invitations?.data?.map((inv) => (
<tr key={inv.id}>
<td>{inv.emailAddress}</td>
<td>{inv.createdAt.toLocaleDateString()}</td>
<td>{inv.role}</td>
<td>
<button
onClick={async () => {
await inv.revoke()
await Promise.all([
memberships?.revalidate,
invitations?.revalidate,
])
}}
>
Revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={!invitations?.hasPreviousPage || invitations?.isFetching}
onClick={() => invitations?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={!invitations?.hasNextPage || invitations?.isFetching}
onClick={() => invitations?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
export const OrgMembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(
OrgMembershipRequestsParams
)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Requested Access</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{membershipRequests?.data?.map((mem) => (
<tr key={mem.id}>
<td>{mem.publicUserData.identifier}</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td></td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={
!membershipRequests?.hasPreviousPage ||
membershipRequests?.isFetching
}
onClick={() => membershipRequests?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={
!membershipRequests?.hasNextPage || membershipRequests?.isFetching
}
onClick={() => membershipRequests?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
export const OrgInviteMemberForm = () => {
const { isLoaded, organization, invitations } = useOrganization(OrgInvitationsParams)
const [emailAddress, setEmailAddress] = useState("")
const [disabled, setDisabled] = useState(false)
if (!isLoaded || !organization) {
return <>Loading</>
}
const onSubmit = async (e) => {
e.preventDefault()
const submittedData = Object.fromEntries(
new FormData(e.currentTarget).entries()
) as {
email: string | undefined
role: OrganizationCustomRoleKey | undefined
}
if (!submittedData.email || !submittedData.role) {
return
}
setDisabled(true)
await organization.inviteMember({
emailAddress: submittedData.email,
role: submittedData.role,
})
await invitations?.revalidate?.()
setEmailAddress("")
setDisabled(false)
}
return (
<form onSubmit={onSubmit}>
<input
name="email"
type="text"
placeholder="Email address"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
<label>Role</label>
<SelectRole fieldName={"role"} />
<button type="submit" disabled={disabled}>
Invite
</button>
</form>
)
}
type SelectRoleProps = {
fieldName?: string
isDisabled?: boolean
onChange?: ChangeEventHandler<HTMLSelectElement>
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { fieldName, isDisabled = false, onChange, defaultRole } = props
const { organization } = useOrganization()
const [fetchedRoles, setRoles] = useState<OrganizationCustomRoleKey[]>([])
const isPopulated = useRef(false)
useEffect(() => {
if (isPopulated.current) return
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRoles(
res.data.map((roles) => roles.key as OrganizationCustomRoleKey)
)
})
}, [organization?.id])
if (fetchedRoles.length === 0) return null
return (
<select
name={fieldName}
disabled={isDisabled}
aria-disabled={isDisabled}
onChange={onChange}
defaultValue={defaultRole}
>
{fetchedRoles?.map((roleKey) => (
<option key={roleKey} value={roleKey}>
{roleKey}
</option>
))}
</select>
)
}
import { useState, useEffect, ChangeEventHandler, useRef } from "react"
import { useOrganization, useUser } from "@clerk/clerk-react"
import type { OrganizationCustomRoleKey } from "@clerk/types"
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
export const OrgInvitationsParams = {
invitations: {
pageSize: 5,
keepPreviousData: true,
},
}
export const OrgMembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// View and manage active organization members,
// along with any pending invitations.
// Invite new members.
export default function Organization() {
const {
organization: currentOrganization,
isLoaded,
} = useOrganization();
if (!isLoaded || !currentOrganization) {
return null;
}
return (
<>
<h1>Organization: {currentOrganization.name}</h1>
<h2 className="mb-6 mt-12">Custom List Invitations</h2>
<OrgInvitations />
<h2 className="mb-6 mt-12">Custom List Membership Requests</h2>
<OrgMembershipRequests />
<h2 className="mb-6 mt-12">Custom List Memberships</h2>
<OrgMembers />
<h2 className="mb-6 mt-12">Custom Invite Form</h2>
<OrgInviteMemberForm />
</>
);
}
// List of organization memberships. Administrators can
// change member roles or remove members from the organization.
export const OrgMembers = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Joined</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{memberships?.data?.map((mem) => (
<tr key={mem.id}>
<td>
{mem.publicUserData.identifier}{" "}
{mem.publicUserData.userId === user?.id && "(You)"}
</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>
<SelectRole
defaultRole={mem.role}
onChange={async (e) => {
await mem.update({
role: e.target.value as OrganizationCustomRoleKey,
})
await memberships?.revalidate()
}}
/>
</td>
<td>
<button
onClick={async () => {
await mem.destroy()
await memberships?.revalidate()
}}
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onClick={() => memberships?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onClick={() => memberships?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
// List of pending invitations to an organization.
// You can invite new organization members and
// revoke already sent invitations.
export const OrgInvitations = () => {
const { isLoaded, invitations, memberships } = useOrganization({
...OrgInvitationsParams,
...OrgMembersParams,
})
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Invited</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{invitations?.data?.map((inv) => (
<tr key={inv.id}>
<td>{inv.emailAddress}</td>
<td>{inv.createdAt.toLocaleDateString()}</td>
<td>{inv.role}</td>
<td>
<button
onClick={async () => {
await inv.revoke()
await Promise.all([
memberships?.revalidate,
invitations?.revalidate,
])
}}
>
Revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={!invitations?.hasPreviousPage || invitations?.isFetching}
onClick={() => invitations?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={!invitations?.hasNextPage || invitations?.isFetching}
onClick={() => invitations?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
export const OrgMembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(
OrgMembershipRequestsParams
)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<table>
<thead>
<tr>
<th>User</th>
<th>Requested Access</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{membershipRequests?.data?.map((mem) => (
<tr key={mem.id}>
<td>{mem.publicUserData.identifier}</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td></td>
</tr>
))}
</tbody>
</table>
<div className="flex">
<button
className="inline-block"
disabled={
!membershipRequests?.hasPreviousPage ||
membershipRequests?.isFetching
}
onClick={() => membershipRequests?.fetchPrevious?.()}
>
Previous
</button>
<button
className="inline-block"
disabled={
!membershipRequests?.hasNextPage || membershipRequests?.isFetching
}
onClick={() => membershipRequests?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
export const OrgInviteMemberForm = () => {
const { isLoaded, organization, invitations } = useOrganization(OrgInvitationsParams)
const [emailAddress, setEmailAddress] = useState("")
const [disabled, setDisabled] = useState(false)
if (!isLoaded || !organization) {
return <>Loading</>
}
const onSubmit = async (e) => {
e.preventDefault()
const submittedData = Object.fromEntries(
new FormData(e.currentTarget).entries()
) as {
email: string | undefined
role: OrganizationCustomRoleKey | undefined
}
if (!submittedData.email || !submittedData.role) {
return
}
setDisabled(true)
await organization.inviteMember({
emailAddress: submittedData.email,
role: submittedData.role,
})
await invitations?.revalidate?.()
setEmailAddress("")
setDisabled(false)
}
return (
<form onSubmit={onSubmit}>
<input
name="email"
type="text"
placeholder="Email address"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
<label>Role</label>
<SelectRole fieldName={"role"} />
<button type="submit" disabled={disabled}>
Invite
</button>
</form>
)
}
type SelectRoleProps = {
fieldName?: string
isDisabled?: boolean
onChange?: ChangeEventHandler<HTMLSelectElement>
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { fieldName, isDisabled = false, onChange, defaultRole } = props
const { organization } = useOrganization()
const [fetchedRoles, setRoles] = useState<OrganizationCustomRoleKey[]>([])
const isPopulated = useRef(false)
useEffect(() => {
if (isPopulated.current) return
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRoles(
res.data.map((roles) => roles.key as OrganizationCustomRoleKey)
)
})
}, [organization?.id])
if (fetchedRoles.length === 0) return null
return (
<select
name={fieldName}
disabled={isDisabled}
aria-disabled={isDisabled}
onChange={onChange}
defaultValue={defaultRole}
>
{fetchedRoles?.map((roleKey) => (
<option key={roleKey} value={roleKey}>
{roleKey}
</option>
))}
</select>
)
}
<p>List of memberships:</p>
<ul id="memberships_list"></ul>
<p>List of invitations:</p>
<ul id="invitations_list"></ul>
<p>Send a new invitation:</p>
<form id="new_invitation">
<div>
<label>Email address</div>
<br />
<input type="email" id="email_address" name="email_address" />
</div>
<button>Invite</button>
</form>
<script>
async function renderMemberships(organization, isAdmin) {
const list = document.getElementById("memberships_list");
try {
const { data: memberships } = await organization.getMemberships();
memberships.map((membership) => {
const li = document.createElement("li");
li.textContent = `${membership.publicUserData.identifier} - ${membership.role}`;
// Add administrative actions; update role and remove member.
if (isAdmin) {
const updateBtn = document.createElement("button");
updateBtn.textContent = "Change role";
updateBtn.addEventListener("click", async function(e) {
e.preventDefault();
const role = membership.role === "admin" ?
"org:member" :
"admin";
await membership.update({ role });
});
li.appendChild(updateBtn);
const removeBtn = document.createElement("button");
removeBtn.textContent = "Remove";
removeBtn.addEventListener("click", async function(e) {
e.preventDefault();
await currentOrganization.removeMember(membership.userId);
});
li.appendChild(removeBtn);
}
// Add the entry to the list
list.appendChild(li);
});
} catch (err) {
console.error(err);
}
}
async function renderInvitations(organization, isAdmin) {
const list = document.getElementById("invitations_list");
try {
const { data: invitations } = await organization.getInvitations();
invitations.map((invitation) => {
const li = document.createElement("li");
li.textContent = `${invitation.emailAddress} - ${invitation.role}`;
// Add administrative actions; revoke invitation
if (isAdmin) {
const revokeBtn = document.createElement("button");
revokeBtn.textContent = "Revoke";
revokeBtn.addEventListener("click", async function(e) {
e.preventDefault();
await invitation.revoke();
});
li.appendChild(revokeBtn);
}
// Add the entry to the list
list.appendChild(li);
});
} catch (err) {
console.error(err);
}
}
async function init() {
// This is the current organization ID.
const organizationId = "org_XXXXXXX";
const { data: organizationMemberships } = await window.Clerk.user.getOrganizationMemberships();
const currentMembership = organizationMemberships.find(membership
=> membership.organization.id === organizationId);
const currentOrganization = currentMembership.organization;
if (!currentOrganization) {
return;
}
const isAdmin = currentMembership.role === "admin";
renderMemberships(currentOrganization, isAdmin);
renderInvitations(currentOrganization, isAdmin);
if (isAdmin) {
const form = document.getElementById("new_invitation");
form.addEventListener("submit", async function(e) {
e.preventDefault();
const inputEl = document.getElementById("email_address");
if (!inputEl) {
return;
}
try {
await currentOrganization.inviteMember({
emailAddress: inputEl.value,
role: "org:member",
});
} catch (err) {
console.error(err);
}
});
}
}
init();
</script>
Feedback
Last updated on