Docs

Build a custom flow for managing organization membership requests

Caution

This guide is for users who want to build a custom user interface using the Clerk API. To manage organization membership requests using a prebuilt UI, you should use Clerk's prebuilt components.

This guide will demonstrate how to use Clerk's 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 active organization's membership requests.

membershipRequests is an object with data that contains an array of OrganizationMembershipRequest objects.

Each OrganizationMembershipRequest object has an accept() and reject() method to accept or reject the membership request, respectively.

This example is written for Next.js App Router but can be adapted for any React meta framework, such as Remix or Gatsby.

app/components/MembershipRequests.tsx
'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>
    </>
  );
};

The following example:

  1. Calls the getMembershipRequests() method to retrieve the list of membership requests for the active organization. This method returns data, which is an array of OrganizationMembershipRequest objects.
  2. Maps over the data array to display the membership requests in a table.
  3. Provides an "Accept" and "Reject" button for each request that calls the accept() and reject() methods, respectively.

Use the 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>

    <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>
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.user) {
  // 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);
}

Feedback

What did you think of this content?