Build a custom flow for managing member Roles in an Organization
Organization members with appropriate Permissions can manage a member's Roles and remove members within an Organization.
This guide will demonstrate how to use the Clerk API to build a custom flow for managing member Roles in an Organization.
The following example:
- Uses the useOrganization() hook to get
memberships, which is a list of the memberships.membershipsis an object withdatathat contains an array ofOrganizationMembershipobjects.- Each
OrganizationMembershipobject has anupdate()anddestroy()method to update the member's Role and remove the member from the Organization, respectively.
- Maps over the
dataarray to display the memberships in a table, providing an "Update Role" and "Remove Member" button for each membership that calls theupdate()anddestroy()methods, respectively.
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
'use client'
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,
},
}
// List of organization memberships. Administrators can
// change member Roles or remove members from the Organization.
export const ManageRoles = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<h1>Memberships List</h1>
<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>
<button
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onClick={() => memberships?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onClick={() => memberships?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}
type SelectRoleProps = {
fieldName?: string
onChange?: ChangeEventHandler<HTMLSelectElement>
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { fieldName, onChange, defaultRole } = props
const { organization } = useOrganization()
const [rolesResponse, setRolesResponse] =
useState < ReturnType < NonNullable < typeof organization > ['getRoles'] >> [0] > []
const isPopulated = useRef(false)
useEffect(() => {
if (isPopulated.current) return
setIsLoading(true)
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRolesResponse(res)
})
}, [organization?.id])
if (rolesResponse.data?.length === 0) return null
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const isDisabled = !!rolesResponse.has_role_set_migration
const roleKeys = rolesResponse.data.map((role) => role.key)
return (
<select
name={fieldName}
disabled={isDisabled}
aria-disabled={isDisabled}
onChange={onChange}
defaultValue={defaultRole}
>
{roleKeys?.map((roleKey) => (
<option key={roleKey} value={roleKey}>
{roleKey}
</option>
))}
</select>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useOrganization, useUser } from '@clerk/clerk-expo'
import type { OrganizationCustomRoleKey } from '@clerk/types'
import * as React from 'react'
import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, View } from 'react-native'
export const OrgMembersParams = {
memberships: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization memberships. Administrators can
// change member Roles or remove members from the Organization.
export const ManageRoles = () => {
const { user } = useUser()
const { isLoaded, memberships } = useOrganization(OrgMembersParams)
if (!isLoaded) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" />
<ThemedText>Loading</ThemedText>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Memberships List
</ThemedText>
{memberships?.data && memberships.data.length > 0 ? (
<>
<ScrollView style={styles.scrollView}>
{memberships?.data?.map((mem) => (
<View key={mem.id} style={styles.card}>
<ThemedText style={styles.label}>User:</ThemedText>
<ThemedText style={styles.value}>
{mem.publicUserData?.identifier}{' '}
{mem.publicUserData?.userId === user?.id && '(You)'}
</ThemedText>
<ThemedText style={styles.label}>Joined:</ThemedText>
<ThemedText style={styles.value}>{mem.createdAt.toLocaleDateString()}</ThemedText>
<ThemedText style={styles.label}>Role:</ThemedText>
<SelectRole
defaultRole={mem.role}
onRoleSelect={async (role) => {
await mem.update({
role: role,
})
await memberships?.revalidate()
}}
/>
<Pressable
style={({ pressed }) => [
styles.button,
styles.removeButton,
pressed && styles.buttonPressed,
]}
onPress={async () => {
await mem.destroy()
await memberships?.revalidate()
}}
>
<ThemedText style={styles.buttonText}>Remove</ThemedText>
</Pressable>
</View>
))}
</ScrollView>
<View style={styles.pagination}>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!memberships?.hasPreviousPage || memberships?.isFetching) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!memberships?.hasPreviousPage || memberships?.isFetching}
onPress={() => memberships?.fetchPrevious?.()}
>
<ThemedText style={styles.buttonText}>Previous</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!memberships?.hasNextPage || memberships?.isFetching) && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!memberships?.hasNextPage || memberships?.isFetching}
onPress={() => memberships?.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Next</ThemedText>
</Pressable>
</View>
</>
) : (
<ThemedText>No memberships found</ThemedText>
)}
</ThemedView>
)
}
type SelectRoleProps = {
onRoleSelect?: (role: OrganizationCustomRoleKey) => void
defaultRole?: string
}
const SelectRole = (props: SelectRoleProps) => {
const { onRoleSelect, defaultRole } = props
const { organization } = useOrganization()
const [rolesResponse, setRolesResponse] =
React.useState < ReturnType < NonNullable < typeof organization > ['getRoles'] >> [0] > []
const [showModal, setShowModal] = React.useState(false)
const [selectedRole, setSelectedRole] = React.useState<OrganizationCustomRoleKey | undefined>(
defaultRole as OrganizationCustomRoleKey | undefined,
)
const isPopulated = React.useRef(false)
React.useEffect(() => {
if (isPopulated.current) return
organization
?.getRoles({
pageSize: 20,
initialPage: 1,
})
.then((res) => {
isPopulated.current = true
setRolesResponse(res)
})
}, [organization?.id])
React.useEffect(() => {
setSelectedRole(defaultRole as OrganizationCustomRoleKey | undefined)
}, [defaultRole])
if (rolesResponse.data?.length === 0) return null
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const isDisabled = !!rolesResponse.has_role_set_migration
const roleKeys = rolesResponse.data.map((role) => role.key)
const handleRoleSelect = (role: OrganizationCustomRoleKey) => {
setSelectedRole(role)
setShowModal(false)
onRoleSelect?.(role)
}
return (
<View>
<Pressable
style={({ pressed }) => [
styles.selectButton,
isDisabled && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={() => !isDisabled && setShowModal(true)}
disabled={isDisabled}
>
<ThemedText style={styles.selectButtonText}>{selectedRole || 'Select role'}</ThemedText>
</Pressable>
<Modal visible={showModal} transparent animationType="slide">
<ThemedView style={styles.modalOverlay}>
<ThemedView style={styles.modalContent}>
<ThemedText type="title" style={styles.modalTitle}>
Select Role
</ThemedText>
<ScrollView>
{roleKeys?.map((roleKey) => (
<Pressable
key={roleKey}
style={({ pressed }) => [
styles.roleOption,
selectedRole === roleKey && styles.roleOptionSelected,
pressed && styles.buttonPressed,
]}
onPress={() => handleRoleSelect(roleKey)}
>
<ThemedText
style={[
styles.roleOptionText,
selectedRole === roleKey && styles.roleOptionTextSelected,
]}
>
{roleKey}
</ThemedText>
</Pressable>
))}
</ScrollView>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={() => setShowModal(false)}
>
<ThemedText style={styles.buttonText}>Cancel</ThemedText>
</Pressable>
</ThemedView>
</ThemedView>
</Modal>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
title: {
marginBottom: 20,
},
label: {
fontWeight: '600',
fontSize: 14,
},
value: {
fontSize: 16,
marginBottom: 8,
},
scrollView: {
flex: 1,
},
card: {
backgroundColor: 'rgba(128, 128, 128, 0.1)',
borderRadius: 8,
padding: 16,
marginBottom: 12,
gap: 8,
},
selectButton: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
backgroundColor: '#fff',
marginBottom: 8,
},
selectButtonText: {
fontSize: 16,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
removeButton: {
backgroundColor: '#dc3545',
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
pagination: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
gap: 12,
},
paginationButton: {
flex: 1,
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContent: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 20,
width: '80%',
maxHeight: '70%',
gap: 16,
},
modalTitle: {
marginBottom: 16,
},
roleOption: {
padding: 12,
borderRadius: 8,
marginBottom: 8,
backgroundColor: '#f0f0f0',
},
roleOptionSelected: {
backgroundColor: '#0a7ea4',
},
roleOptionText: {
fontSize: 16,
},
roleOptionTextSelected: {
color: '#fff',
fontWeight: '600',
},
})The following example includes a checkAdminAndRenderMemberships() function that checks if the user is an admin of the currently active Organization and calls renderMemberships(). The renderMemberships() function lists the Organization's memberships and allows administrators to update a member's Role and remove a member from the Organization.
Use the following tabs to view the code necessary for the index.html and main.js files.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clerk + JavaScript App</title>
</head>
<body>
<div id="app"></div>
<h1>Memberships List</h1>
<table>
<thead>
<tr>
<th>User ID</th>
<th>User identifier</th>
<th>Joined</th>
<th>Role</th>
<th id="update-role-head" hidden>Update role</th>
<th id="remove-member-head" hidden>Remove member</th>
</tr>
</thead>
<tbody id="memberships-table-body"></tbody>
</table>
<script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
</body>
</html>import { Clerk } from '@clerk/clerk-js'
const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!pubKey) {
throw new Error('Add your VITE_CLERK_PUBLISHABLE_KEY to .env file')
}
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY')
await clerk.load()
if (clerk.isSignedIn) {
// Check for an Active Organization
if (clerk.organization) {
// Render list of organization memberships
async function renderMemberships(organization, isAdmin) {
try {
const { data } = await organization.getMemberships()
const memberships = data
console.log(`getMemberships:`, memberships)
// When `has_role_set_migration` is `true`, updating organization membership roles is not allowed.
const rolesResponse = await organization.getRoles()
const isRolesMigrationDisabled = rolesResponse.has_role_set_migration
memberships.map((membership) => {
const membershipTable = document.getElementById('memberships-table-body')
const row = membershipTable.insertRow()
row.insertCell().textContent = membership.publicUserData.userId
row.insertCell().textContent = membership.publicUserData.identifier
row.insertCell().textContent = membership.createdAt.toLocaleDateString()
row.insertCell().textContent = membership.role
// Add administrative actions:
// Add and remove a member, and update a member's Role.
if (isAdmin) {
// Show update and remove member buttons
document.getElementById('update-role-head').removeAttribute('hidden')
document.getElementById('remove-member-head').removeAttribute('hidden')
// Get the user ID of the member
const userId = membership.publicUserData.userId
// Update a member's Role
const updateBtn = document.createElement('button')
updateBtn.textContent = 'Change role'
updateBtn.disabled = isRolesMigrationDisabled
updateBtn.addEventListener('click', async function (e) {
e.preventDefault()
if (isRolesMigrationDisabled) {
return
}
const role = membership.role === 'org:admin' ? 'org:member' : 'org:admin'
await organization
.updateMember({ userId, role })
.then((res) => console.log(`updateMember response:`, res))
.catch((error) => console.log('An error occurred:', error))
})
row.insertCell().appendChild(updateBtn)
// Remove a member
const removeBtn = document.createElement('button')
removeBtn.textContent = 'Remove'
removeBtn.addEventListener('click', async function (e) {
e.preventDefault()
await organization
.removeMember(userId)
.then((res) => console.log(`removeMember response:`, res))
.catch((error) => console.log('An error occurred:', error))
})
row.insertCell().appendChild(removeBtn)
}
})
} catch (error) {
console.log('An error occurred:', error)
}
}
/**
* Checks if a user is an admin of the
* currently Active Organization and
* renders the Organization's memberships.
*/
async function checkAdminAndRenderMemberships() {
const organizationId = clerk.organization.id
const { data } = await clerk.user.getOrganizationMemberships()
const organizationMemberships = data
const currentMembership = organizationMemberships.find(
(membership) => membership.organization.id === organizationId,
)
const currentOrganization = currentMembership.organization
if (!currentOrganization) return
const isAdmin = currentMembership.role === 'org:admin'
console.log(`Current organization:`, currentOrganization)
renderMemberships(currentOrganization, isAdmin)
}
checkAdminAndRenderMemberships()
} else {
// If there is no Active Organization,
// mount Clerk's <OrganizationSwitcher />
// to allow the user to set an Organization as active
document.getElementById('app').innerHTML = `
<h2>Select an Organization to set it as active</h2>
<div id="org-switcher"></div>
`
const orgSwitcherDiv = document.getElementById('org-switcher')
clerk.mountOrganizationSwitcher(orgSwitcherDiv)
}
} else {
// If there is no active user, mount Clerk's <SignIn />
document.getElementById('app').innerHTML = `
<div id="sign-in"></div>
`
const signInDiv = document.getElementById('sign-in')
clerk.mountSignIn(signInDiv)
}Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.
Feedback
Last updated on