Skip to main content
Docs

Build a custom flow for switching Organizations

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

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:

  1. Use the useOrganizationList() hook to get memberships, which is a list of the current user's Organization memberships. memberships returns data, which is an array of OrganizationMembership objects.
  2. Map over the data array to display the user's Organization memberships in a table, providing a button that calls setActive() to set the selected Organization as 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 data array is paginated and will only return the first 5 results, so the fetchNext() method is used to load more Organizations if they are available.
  • The "Infinite list" example sets the infinite option to true to 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.

app/components/CustomOrganizationSwitcher.tsx
'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>
      )}
    </>
  )
}
app/components/CustomOrganizationSwitcher.tsx
'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>
      )}
    </>
  )
}
app/components/organization-switcher.tsx
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,
  },
})
app/components/organization-switcher.tsx
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:

  1. Calls the getOrganizationMemberships() method to retrieve the list of Organizations the current user is a part of. This method returns data, which is an array of OrganizationMembership objects.
  2. Maps over the data array to display the user's Organization memberships in a list, providing a button that calls setActive() to set the selected Organization as the .

Use the following tabs to view the code necessary for the index.html and main.js files.

index.html
<!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>
main.js
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

What did you think of this content?

Last updated on

GitHubEdit on GitHub