Skip to content

feat(ui): Expose profile sub components#8654

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

feat(ui): Expose profile sub components#8654
alexcarpenter wants to merge 21 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: aee47f8

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 Error Error May 29, 2026 2:18pm

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 11 commits May 28, 2026 09:49
Add 'use client' to the experimental entrypoint and move the
document.querySelector call in StyleCacheProvider from module scope
into the useMemo to avoid crashes during server-side rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alexcarpenter alexcarpenter changed the base branch from carp/profile-page-components to main May 28, 2026 19:04
@alexcarpenter alexcarpenter changed the title feat(ui): Expose profile section components feat(ui): Expose profile sub components May 29, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8654

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8654

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8654

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8654

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8654

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8654

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8654

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8654

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8654

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8654

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8654

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8654

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8654

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8654

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8654

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8654

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8654

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8654

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8654

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8654

commit: aee47f8


let storedModuleManager: ModuleManager | undefined;

export function setModuleManager(mm: ModuleManager): void {
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.

Module-level store so composed components can access the ModuleManager that ClerkUI receives at mount time. Composed components render outside the normal component tree so they cant inherit it through context from the mount call — ClerkUI.init() calls setModuleManager() and providers read it back via getModuleManager().

@@ -0,0 +1,30 @@
import type { RouteContextValue } from '../router/RouteContext';

export function createComposedRouter(clerkNavigate: (to: string) => Promise<unknown> | void): RouteContextValue {
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.

Composed components render individual pages, not full routed flows — but many child components still call useRouter() internally. This satisfies that contract with a minimal implementation that delegates to clerk.navigate (or window.location.assign for the static fallback). No actual routing; just enough to prevent crashes from missing context.


export const StyleCacheProvider = (props: StyleCacheProviderProps) => {
const cache = useMemo(() => {
const el = typeof document !== 'undefined' ? document.querySelector('style#cl-style-insertion-point') : null;
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.

Moved document.querySelector inside the component body. Top-level call ran at import time which breaks SSR / non-browser environments. Composed components may be imported in SSR-capable frameworks so this needs to be safe.

const { applyVariants, filterProps } = createVariants(theme => {
return {
base: {
boxSizing: 'border-box',
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.

Fixing a sizing issue uncovered where the border rendered outside the size defined causing layout shifts.

const isMountedRef = React.useRef(true);

React.useEffect(() => {
isMountedRef.current = true;
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.

Ref initializes as true but under StrictMode the cleanup runs between double-invocation, setting it to false — without this line the second effect never resets it, so setState becomes a permanent no-op.

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.

1 participant