Build a custom flow for switching Organizations
This guide will demonstrate how to use the Clerk API to build a custom flow for switching between Organizations.
Two examples are provided: one for a paginated list and one for an infinite list.
The following examples:
- Use the useOrganizationList() hook to get
memberships, which is a list of the current user's Organization memberships.membershipsreturnsdata, which is an array ofOrganizationMembershipobjects. - Map over the
dataarray to display the user's Organization memberships in a table, providing a button that callssetActive()to set the selected Organization as the .- If there are no Organizations, the
<CreateOrganization />component (custom-flow version, not the Clerk component) is rendered to allow the user to create an Organization.
- If there are no Organizations, the
The difference between the two examples is the parameters passed to the useOrganizationList() hook in order to determine how the list is paginated.
- The "Paginated list" example provides a button to load more Organizations if there are more available. The
dataarray is paginated and will only return the first 5 results, so thefetchNext()method is used to load more Organizations if they are available. - The "Infinite list" example sets the
infiniteoption totrueto enable infinite results.
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 { useAuth, useOrganizationList } from '@clerk/nextjs'
import CreateOrganization from '../components/create-organization' // See https://clerk.com/docs/guides/development/custom-flows/organizations/create-organizations for this component
// List user's organization memberships
export default function JoinedOrganizations() {
const { isLoaded, setActive, userMemberships } = useOrganizationList({
userMemberships: {
// Set pagination parameters
pageSize: 5,
keepPreviousData: true,
},
})
const { orgId } = useAuth()
if (!isLoaded) {
return <p>Loading...</p>
}
return (
<>
<h1>Joined organizations</h1>
{userMemberships?.data?.length > 0 && (
<>
<table>
<thead>
<tr>
<th>Identifier</th>
<th>Organization</th>
<th>Joined</th>
<th>Role</th>
<th>Set as active org</th>
</tr>
</thead>
<tbody>
{userMemberships?.data?.map((mem) => (
<tr key={mem.id}>
<td>{mem.publicUserData.identifier}</td>
<td>{mem.organization.name}</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>{mem.role}</td>
<td>
{orgId === mem.organization.id ? (
<button onClick={() => setActive({ organization: mem.organization.id })}>
Set as active
</button>
) : (
<p>Currently active</p>
)}
</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={!userMemberships?.hasPreviousPage || userMemberships?.isFetching}
onClick={() => userMemberships?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!userMemberships?.hasNextPage || userMemberships?.isFetching}
onClick={() => userMemberships?.fetchNext?.()}
>
Next
</button>
</div>
</>
)}
{userMemberships?.data?.length === 0 && (
<div>
<p>No organizations found</p>
<CreateOrganization />
</div>
)}
</>
)
}'use client'
import { useAuth, useOrganizationList } from '@clerk/nextjs'
import CreateOrganization from '../components/create-organization' // See https://clerk.com/docs/guides/development/custom-flows/organizations/create-organizations for this component
// List user's organization memberships
export default function JoinedOrganizations() {
const { isLoaded, setActive, userMemberships } = useOrganizationList({
userMemberships: {
// Set pagination parameters
infinite: true,
},
})
const { orgId } = useAuth()
if (!isLoaded) {
return <p>Loading...</p>
}
return (
<>
<h1>Joined organizations</h1>
{userMemberships?.data?.length > 0 && (
<table>
<thead>
<tr>
<th>Identifier</th>
<th>Organization</th>
<th>Joined</th>
<th>Role</th>
<th>Set as active org</th>
</tr>
</thead>
<tbody>
{userMemberships?.data?.map((mem) => (
<tr key={mem.id}>
<td>{mem.publicUserData.identifier}</td>
<td>{mem.organization.name}</td>
<td>{mem.createdAt.toLocaleDateString()}</td>
<td>{mem.role}</td>
<td>
{orgId === mem.organization.id ? (
<button onClick={() => setActive({ organization: mem.organization.id })}>
Set as active
</button>
) : (
<p>Currently active</p>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
{userMemberships?.data?.length === 0 && (
<div>
<p>No organizations found</p>
<CreateOrganization />
</div>
)}
</>
)
}import { CreateOrganization } from '@/components/create-organization' // See https://clerk.com/docs/guides/development/custom-flows/organizations/create-organizations for this component
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useAuth, useOrganizationList } from '@clerk/clerk-expo'
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native'
// List user's organization memberships
export const OrganizationSwitcher = () => {
const { isLoaded, setActive, userMemberships } = useOrganizationList({
userMemberships: {
// Set pagination parameters
pageSize: 5,
keepPreviousData: true,
},
})
const { orgId } = useAuth()
if (!isLoaded) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
<ThemedText>Loading...</ThemedText>
</View>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Joined organizations
</ThemedText>
{userMemberships?.data?.length > 0 && (
<>
<ScrollView style={styles.scrollView}>
{userMemberships?.data?.map((mem) => (
<View key={mem.id} style={styles.card}>
<ThemedText style={styles.label}>Identifier:</ThemedText>
<ThemedText style={styles.value}>
{mem.publicUserData?.identifier || 'N/A'}
</ThemedText>
<ThemedText style={styles.label}>Organization:</ThemedText>
<ThemedText style={styles.value}>{mem.organization.name}</ThemedText>
<ThemedText style={styles.label}>Joined:</ThemedText>
<ThemedText style={styles.value}>{mem.createdAt.toLocaleDateString()}</ThemedText>
<ThemedText style={styles.label}>Role:</ThemedText>
<ThemedText style={styles.value}>{mem.role}</ThemedText>
<View style={styles.buttonContainer}>
{orgId === mem.organization.id ? (
<ThemedText style={styles.activeText}>Currently active</ThemedText>
) : (
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={() => setActive({ organization: mem.organization.id })}
>
<ThemedText style={styles.buttonText}>Set as active</ThemedText>
</Pressable>
)}
</View>
</View>
))}
</ScrollView>
<View style={styles.pagination}>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!userMemberships?.hasPreviousPage || userMemberships?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!userMemberships?.hasPreviousPage || userMemberships?.isFetching}
onPress={() => userMemberships?.fetchPrevious?.()}
>
<ThemedText style={styles.buttonText}>Previous</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!userMemberships?.hasNextPage || userMemberships?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!userMemberships?.hasNextPage || userMemberships?.isFetching}
onPress={() => userMemberships?.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Next</ThemedText>
</Pressable>
</View>
</>
)}
{userMemberships?.data?.length === 0 && (
<View style={styles.emptyContainer}>
<ThemedText>No organizations found</ThemedText>
<CreateOrganization />
</View>
)}
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
title: {
marginBottom: 20,
},
scrollView: {
flex: 1,
},
card: {
backgroundColor: 'rgba(128, 128, 128, 0.1)',
borderRadius: 8,
padding: 16,
marginBottom: 12,
gap: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
value: {
fontSize: 16,
marginBottom: 8,
},
buttonContainer: {
marginTop: 8,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
activeText: {
color: '#0a7ea4',
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',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 20,
},
})import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useAuth, useOrganizationList } from '@clerk/clerk-expo'
import { FlatList, Pressable, ScrollView, StyleSheet, View } from 'react-native'
// List user's organization memberships
export default function JoinedOrganizations() {
const { isLoaded, setActive, userMemberships } = useOrganizationList({
userMemberships: {
// Set pagination parameters
infinite: true,
},
})
const { orgId } = useAuth()
if (!isLoaded) {
return (
<ThemedView style={styles.container}>
<ThemedText>Loading...</ThemedText>
</ThemedView>
)
}
return (
<ScrollView>
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Joined organizations
</ThemedText>
{userMemberships?.data && userMemberships.data.length > 0 ? (
<FlatList
data={userMemberships.data}
scrollEnabled={false}
keyExtractor={(item) => item.id}
renderItem={({ item: mem }) => (
<View style={styles.organizationCard}>
<View style={styles.organizationInfo}>
<ThemedText style={styles.organizationName}>{mem.organization.name}</ThemedText>
<ThemedText style={styles.detailText}>
Identifier: {mem.publicUserData?.identifier || 'N/A'}
</ThemedText>
<ThemedText style={styles.detailText}>
Joined: {mem.createdAt.toLocaleDateString()}
</ThemedText>
<ThemedText style={styles.detailText}>Role: {mem.role}</ThemedText>
</View>
<View style={styles.organizationActions}>
{orgId !== mem.organization.id ? (
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={() => setActive({ organization: mem.organization.id })}
>
<ThemedText style={styles.buttonText}>Set as active</ThemedText>
</Pressable>
) : (
<View style={styles.activeContainer}>
<ThemedText style={styles.activeText}>Currently active</ThemedText>
</View>
)}
</View>
</View>
)}
/>
) : (
<View style={styles.emptyContainer}>
<ThemedText style={styles.emptyText}>No organizations found</ThemedText>
</View>
)}
</ThemedView>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
marginBottom: 16,
},
organizationCard: {
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 8,
marginBottom: 12,
gap: 12,
},
organizationInfo: {
gap: 6,
},
organizationName: {
fontSize: 18,
fontWeight: '600',
},
detailText: {
fontSize: 14,
opacity: 0.8,
},
organizationActions: {
marginTop: 4,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 6,
alignItems: 'center',
},
buttonPressed: {
opacity: 0.7,
},
buttonText: {
color: '#fff',
fontWeight: '600',
fontSize: 14,
},
activeContainer: {
padding: 10,
backgroundColor: '#e8f5e9',
borderRadius: 6,
alignItems: 'center',
},
activeText: {
color: '#2e7d32',
fontWeight: '600',
fontSize: 14,
},
emptyContainer: {
padding: 20,
gap: 16,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
opacity: 0.8,
},
})The following example:
- Calls the
getOrganizationMemberships()method to retrieve the list of Organizations the current user is a part of. This method returnsdata, which is an array ofOrganizationMembershipobjects. - Maps over the
dataarray to display the user's Organization memberships in a list, providing a button that callssetActive()to set the selected Organization as the .
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>
<h2>Joined organizations</h2>
<table>
<thead>
<tr>
<th>Identifier</th>
<th>Organization</th>
<th>Joined</th>
<th>Role</th>
<th>Set as active org</th>
</tr>
</thead>
<tbody id="memberships-table-body"></tbody>
</table>
<div id="create-organization-container" hidden>
<h1>Create an organization</h1>
<form id="create-organization">
<label for="name">Name</label>
<input id="name" name="name" />
<button>Create organization</button>
</form>
</div>
<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) {
const { data } = await clerk.user.getOrganizationMemberships()
const memberships = data
memberships.map((membership) => {
const membershipTable = document.getElementById('memberships-table-body')
const row = membershipTable.insertRow()
row.insertCell().textContent = membership.publicUserData.identifier
row.insertCell().textContent = membership.organization.name
row.insertCell().textContent = membership.createdAt.toLocaleDateString()
row.insertCell().textContent = membership.role
// Set as Active Organization
const addBtn = document.createElement('button')
addBtn.textContent = 'Set as active'
addBtn.addEventListener('click', async function (e) {
e.preventDefault()
await clerk
.setActive({ organization: membership.organization.id })
.then((res) => {
console.log('Set as active:', res)
})
.catch((err) => {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
})
})
row.insertCell().appendChild(addBtn)
})
} else {
// If there is no Active Organization,
// render a form to create an Organization
document.getElementById('create-organization-container').removeAttribute('hidden')
const form = document.getElementById('create-organization')
form.addEventListener('submit', function (e) {
e.preventDefault()
const inputEl = document.getElementById('name')
if (!inputEl) {
// ... handle empty input
return
}
clerk
.createOrganization({ name: inputEl.value })
.then((res) => console.log(res))
.catch((error) => console.log('An error occurred:', error))
})
}
} else {
// If there is no active user, mount Clerk's <SignIn />
// or add your sign-in custom flow
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