Skip to content

feat(ui): Expose profile sub components#8654

Draft
alexcarpenter wants to merge 28 commits into
mainfrom
carp/profile-section-components
Draft

feat(ui): Expose profile sub components#8654
alexcarpenter wants to merge 28 commits into
mainfrom
carp/profile-section-components

Conversation

@alexcarpenter
Copy link
Copy Markdown
Member

@alexcarpenter alexcarpenter commented May 26, 2026

Summary

Exposes UserProfile and OrganizationProfile as composable sub-components from @clerk/ui/experimental. Lets consumers render individual profile sections (Account, Security, Members, Billing, etc.) outside the full modal/page flow.

Approach

  • New composed/ directory — each profile gets a Provider that wires up the required context tree (appearance, environment, module manager, routing, flow metadata) so individual section components render standalone.
  • moduleManagerStore — module-level get/set because composed components mount outside the normal component tree and can't inherit ModuleManager via context from ClerkUI.init().
  • stubRouter — minimal RouteContext implementation delegating to clerk.navigate. Child components call useRouter() internally but composed pages don't do real routing.
  • StyleCacheProvider — moved document.querySelector from module scope into the component body; import-time access breaks SSR.
  • useSafeState — reset isMountedRef to true in effect body; StrictMode cleanup between double-invocation left it permanently false.
  • Animated — guard against StrictMode double-mount leaving animation refs stale.
  • Exported via @clerk/ui/experimental with 'use client' directive.

API

import { UserProfile, OrganizationProfile } from '@clerk/ui/experimental';

<UserProfile>
  <UserProfile.Account />
  <UserProfile.Security />
</UserProfile>

<OrganizationProfile>
  <OrganizationProfile.General />
  <OrganizationProfile.Members />
</OrganizationProfile>

Sub-components available: Account, Security, Billing, APIKeys (user); General, Members, Billing, APIKeys, ConfigureSSO (org). Fine-grained wrappers also exported (e.g. AccountProfile, SecurityPasskeys).

Test plan

  • Composed provider wiring tests verify context parity with full-flow mounts
  • Section-level render tests for both UserProfile and OrganizationProfile
  • StrictMode animation + state tests
  • Stub router limitation tests

🤖 Generated with Claude Code

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

⚠️ No Changeset found

Latest commit: 40e270d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 2, 2026 6:33pm

Request Review

@alexcarpenter
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

@alexcarpenter
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

@alexcarpenter
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

@alexcarpenter
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions

This comment has been minimized.

@alexcarpenter
Copy link
Copy Markdown
Member Author

!snapshot

@github-actions
Copy link
Copy Markdown
Contributor

Hey @alexcarpenter - the snapshot version command generated the following package versions:

Package Version
@clerk/astro 3.3.0-snapshot.v20260527161602
@clerk/backend 3.4.12-snapshot.v20260527161602
@clerk/chrome-extension 3.1.29-snapshot.v20260527161602
@clerk/clerk-js 6.12.0-snapshot.v20260527161602
@clerk/dev-cli 0.1.1-snapshot.v20260527161602
@clerk/expo 3.2.15-snapshot.v20260527161602
@clerk/expo-passkeys 1.0.28-snapshot.v20260527161602
@clerk/express 2.1.20-snapshot.v20260527161602
@clerk/fastify 3.1.30-snapshot.v20260527161602
@clerk/hono 0.1.30-snapshot.v20260527161602
@clerk/localizations 4.6.7-snapshot.v20260527161602
@clerk/msw 0.0.28-snapshot.v20260527161602
@clerk/nextjs 7.4.0-snapshot.v20260527161602
@clerk/nuxt 2.5.0-snapshot.v20260527161602
@clerk/react 6.7.0-snapshot.v20260527161602
@clerk/react-router 3.3.0-snapshot.v20260527161602
@clerk/shared 4.13.0-snapshot.v20260527161602
@clerk/tanstack-react-start 1.3.0-snapshot.v20260527161602
@clerk/testing 2.0.32-snapshot.v20260527161602
@clerk/ui 1.13.0-snapshot.v20260527161602
@clerk/upgrade 2.0.3-snapshot.v20260527161602
@clerk/vue 2.3.0-snapshot.v20260527161602

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/astro

npm i @clerk/astro@3.3.0-snapshot.v20260527161602 --save-exact

@clerk/backend

npm i @clerk/backend@3.4.12-snapshot.v20260527161602 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@3.1.29-snapshot.v20260527161602 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@6.12.0-snapshot.v20260527161602 --save-exact

@clerk/dev-cli

npm i @clerk/dev-cli@0.1.1-snapshot.v20260527161602 --save-exact

@clerk/expo

npm i @clerk/expo@3.2.15-snapshot.v20260527161602 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@1.0.28-snapshot.v20260527161602 --save-exact

@clerk/express

npm i @clerk/express@2.1.20-snapshot.v20260527161602 --save-exact

@clerk/fastify

npm i @clerk/fastify@3.1.30-snapshot.v20260527161602 --save-exact

@clerk/hono

npm i @clerk/hono@0.1.30-snapshot.v20260527161602 --save-exact

@clerk/localizations

npm i @clerk/localizations@4.6.7-snapshot.v20260527161602 --save-exact

@clerk/msw

npm i @clerk/msw@0.0.28-snapshot.v20260527161602 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@7.4.0-snapshot.v20260527161602 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@2.5.0-snapshot.v20260527161602 --save-exact

@clerk/react

npm i @clerk/react@6.7.0-snapshot.v20260527161602 --save-exact

@clerk/react-router

npm i @clerk/react-router@3.3.0-snapshot.v20260527161602 --save-exact

@clerk/shared

npm i @clerk/shared@4.13.0-snapshot.v20260527161602 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@1.3.0-snapshot.v20260527161602 --save-exact

@clerk/testing

npm i @clerk/testing@2.0.32-snapshot.v20260527161602 --save-exact

@clerk/ui

npm i @clerk/ui@1.13.0-snapshot.v20260527161602 --save-exact

@clerk/upgrade

npm i @clerk/upgrade@2.0.3-snapshot.v20260527161602 --save-exact

@clerk/vue

npm i @clerk/vue@2.3.0-snapshot.v20260527161602 --save-exact

alexcarpenter and others added 12 commits May 29, 2026 12:55
Annotate every exported composed component with a ReactNode return
type so the generated .d.ts files are compatible with both React 18
and React 19 consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export UserProfile and OrganizationProfile as function components
with static properties (via Object.assign) instead of plain objects.
This matches the existing UserButton pattern and is compatible with
React Server Components client references, which support property
access on function exports but not on plain object exports.

The provider is now the root component itself — consumers write
<UserProfile> instead of <UserProfile.Provider>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The spy returned undefined in jsdom, causing autoAnimate's
MutationObserver callback to crash on .addEventListener.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jacekradko
Copy link
Copy Markdown
Member

I like this from a directional standpoint and I agree that exposing subcomponents makes sense for flexibility and customization. At a quick glance, though, this seems to duplicate/add another Profile component stack that then needs extra context plumbed into it to work. That expands our public/internal contract and likely increases the chance of drift and defects over time. Could we instead look at extracting the shared parts from the current Profile components so the existing implementation and the new subcomponent API compose the same internals differently? What do you think? Happy to collab on this if you want or drop a high level proposal for internals

@alexcarpenter
Copy link
Copy Markdown
Member Author

@jacekradko here is an update with shared parts targeting this branch #8714

Copy link
Copy Markdown
Member

@Ephem Ephem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to think more on the clerk-js/ui boundary, but I thought I'd leave some random thoughts.

} from './sectionWrappers';
import { ConfigureSSO } from './ConfigureSSO';

export const OrganizationProfile = Object.assign(OrganizationProfileProvider, {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as here: #8474 (comment)

These are meant to be publicly exported, so do we optimize for treeshaking early even though that's annoying for compound components?

Comment thread packages/ui/src/composed/OrganizationProfile/sectionWrappers.tsx

export const APIKeys = (): ReactNode => (
<CardStateProvider>
<Suspense fallback={''}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The <Suspense> fallbacks are interesting. 🤔 I think designing the loading and error experience should be front and center here.

At what levels do we handle it? (Page, section, component?) How do we make it good out of the box, while also allowing devs to tweak fallbacks?

This is tricky since we really only use Suspense for the initialState now, so maybe the thinking and right move is to just do '' for now? Maybe we don't allow for tweaking loading states besides CSS in the first iteration?

It would be interesting to figure out the best possible long term DX here even if we don't implement it right away. I think that's Suspense+Error boundaries for everything, so the user can control it.

Just thinking out loud, but maybe that's also composable? Also maybe they want a top level loading state for their entire page/section of the page where Clerk is a small part, and that loading state lives far away from the Clerk code? They don't want the rest of the page to reveal first, show Clerk loading state, reveal Clerk, they want to wait for everything to load before revealing, including Clerk? Especially now that they can compose together their UI.

This of course means we can't render the components in their own React application, it needs to compose together with the host app, but I don't think this PR is doing the standalone render right?

Comment thread packages/ui/src/composed/UserProfile/sectionWrappers.tsx Outdated

type UserProfileProviderProps = React.PropsWithChildren<{
appearance?: Appearance;
additionalOAuthScopes?: Partial<Record<OAuthProvider, OAuthScope[]>>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these components keep rendering inside the host app, as opposed to as a separate React app, maybe SSR becomes as "easy" as passing in an initialState prop here, which we put on a context the hooks read from further down? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibility! I think @jacekradko is looking at bringing these back under the same runtime here #8718

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at that, and if I understood it correctly, the components were still rendering as part of the host app, and not as a separate app?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants