Build a custom flow for managing Organization membership requests
This guide will demonstrate how to use the Clerk API to build a custom flow for managing Organization membership requests.
The following example:
- Uses the useOrganization() hook to get
membershipRequests, which is a list of the membership requests.membershipRequestsis an object withdatathat contains an array ofOrganizationMembershipRequestobjects.- Each
OrganizationMembershipRequestobject has anaccept()andreject()method to accept or reject the membership request, respectively.
- Maps over the
dataarray to display the membership requests in a table, providing an "Accept" and "Reject" button for each request that calls theaccept()andreject()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 { useOrganization } from '@clerk/nextjs'
export const MembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization membership requests.
export const MembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(MembershipRequestsParams)
if (!isLoaded) {
return <>Loading</>
}
return (
<>
<h1>Membership requests</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Date requested</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>
<button
onClick={async () => {
await mem.accept()
}}
>
Accept
</button>
<button
onClick={async () => {
await mem.reject()
}}
>
Reject
</button>
</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching}
onClick={() => membershipRequests?.fetchPrevious?.()}
>
Previous
</button>
<button
disabled={!membershipRequests?.hasNextPage || membershipRequests?.isFetching}
onClick={() => membershipRequests?.fetchNext?.()}
>
Next
</button>
</div>
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useOrganization } from '@clerk/clerk-expo'
import * as React from 'react'
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native'
export const MembershipRequestsParams = {
membershipRequests: {
pageSize: 5,
keepPreviousData: true,
},
}
// List of organization membership requests.
export const MembershipRequests = () => {
const { isLoaded, membershipRequests } = useOrganization(MembershipRequestsParams)
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}>
Membership requests
</ThemedText>
{membershipRequests?.data && membershipRequests.data.length > 0 ? (
<>
<ScrollView style={styles.scrollView}>
{membershipRequests?.data?.map((mem) => (
<View key={mem.id} style={styles.card}>
<ThemedText style={styles.label}>User:</ThemedText>
<ThemedText style={styles.value}>
{mem.publicUserData?.identifier || 'N/A'}
</ThemedText>
<ThemedText style={styles.label}>Date requested:</ThemedText>
<ThemedText style={styles.value}>{mem.createdAt.toLocaleDateString()}</ThemedText>
<View style={styles.buttonRow}>
<Pressable
style={({ pressed }) => [styles.acceptButton, pressed && styles.buttonPressed]}
onPress={async () => {
await mem.accept()
}}
>
<ThemedText style={styles.buttonText}>Accept</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.rejectButton, pressed && styles.buttonPressed]}
onPress={async () => {
await mem.reject()
}}
>
<ThemedText style={styles.buttonText}>Reject</ThemedText>
</Pressable>
</View>
</View>
))}
</ScrollView>
<View style={styles.pagination}>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!membershipRequests?.hasPreviousPage || membershipRequests?.isFetching}
onPress={() => membershipRequests?.fetchPrevious?.()}
>
<ThemedText style={styles.buttonText}>Previous</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.paginationButton,
(!membershipRequests?.hasNextPage || membershipRequests?.isFetching) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
disabled={!membershipRequests?.hasNextPage || membershipRequests?.isFetching}
onPress={() => membershipRequests?.fetchNext?.()}
>
<ThemedText style={styles.buttonText}>Next</ThemedText>
</Pressable>
</View>
</>
) : (
<ThemedText>No membership requests</ThemedText>
)}
</ThemedView>
)
}
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,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginTop: 8,
},
acceptButton: {
flex: 1,
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
rejectButton: {
flex: 1,
backgroundColor: '#dc3545',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
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',
},
})The following example:
- Calls the
getMembershipRequests()method to retrieve the list of membership requests for the . This method returnsdata, which is an array ofOrganizationMembershipRequestobjects. - Maps over the
dataarray to display the membership requests in a table. - Provides an "Accept" and "Reject" button for each request that calls the
accept()andreject()methods, respectively.
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>Membership Requests</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Date requested</th>
<th>Accept</th>
<th>Reject</th>
</tr>
</thead>
<tbody id="requests-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) {
const requestsTable = document.getElementById('requests-table-body')
const { data } = await clerk.organization
.getMembershipRequests()
.then((res) => console.log(`Membership requests:`, data).catch((err) => console.error(err)))
const requests = data
requests.map((request) => {
const row = requestsTable.insertRow()
row.insertCell().textContent = request.publicUserData.identifier
row.insertCell().textContent = request.createdAt.toLocaleDateString()
// Accept request
const acceptBtn = document.createElement('button')
acceptBtn.textContent = 'Accept'
acceptBtn.addEventListener('click', async function (e) {
e.preventDefault()
await request.accept()
})
row.insertCell().appendChild(acceptBtn)
// Reject request
const rejectBtn = document.createElement('button')
rejectBtn.textContent = 'Reject'
rejectBtn.addEventListener('click', async function (e) {
e.preventDefault()
await request.reject()
})
row.insertCell().appendChild(rejectBtn)
})
} 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