Skip to content

Conversation

@Ephem
Copy link

@Ephem Ephem commented Nov 20, 2025

Description

This is a proof of concept that reimagines a few things. It builds on #7194 but takes things quite a bit further.

Note

I'm targeting this directly at vincent-and-the-doctor and not the PR above. That means this PR contains some of the same changes as that one, but since it also reworks things quite a bit I choose to target it this way so this can be reviewed entirely stand alone.

There are a lot of details in here, but in essence, what this PR does is:

  • Merge the ContextProviders from ui and react into shared/react
  • Removes the clerk.addListener() in that provider, also removes most contexts
    • Breaking change: Several context providers like UserContext are no longer exported from shared/react
  • Adds new hooks that subscribes directly to clerk via useSyncExternalStore
    • These "base" hooks, like useUserBase are also reexported as useUserContext to avoid breaking change for these
    • They have the exact same signature, leaving all other hooks untouched
  • initialState can now be a promise
    • <ClerkProvider dynamic> in the Next package no longer does await initialState

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Ephem added 27 commits November 10, 2025 14:35
@changeset-bot
Copy link

changeset-bot bot commented Nov 20, 2025

⚠️ No Changeset found

Latest commit: c3c79f9

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

vercel bot commented Nov 20, 2025

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

Project Deployment Preview Comments Updated (UTC)
clerk-js-sandbox Ready Ready Preview Comment Nov 20, 2025 1:58pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fredrik/poc

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Comment @coderabbitai help to get the list of available commands and usage tips.

this.#listeners.push(listener);
// emit right away
if (this.client) {
if (this.client && !options?.skipInitialEmit) {
Copy link
Author

Choose a reason for hiding this comment

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

uSES reads state directly from clerk, so this was introduced to avoid extra emits every time addListener was called in a hook.

const propsWithEnvs = mergeNextClerkPropsWithEnv({
...rest,
initialState: statePromiseOrValue as InitialState | Promise<InitialState> | undefined,
nonce: await noncePromiseOrValue,
Copy link
Author

Choose a reason for hiding this comment

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

Awaiting this is something we might also want to move further down.

Copy link
Author

Choose a reason for hiding this comment

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

This should likely be renamed now that it doesn't actually contain a context.


export { ClerkProvider };

const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => {
Copy link
Author

Choose a reason for hiding this comment

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

This was moved up from the now deleted ClerkContextProvider, but code is unchanged.

Copy link
Author

Choose a reason for hiding this comment

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

Diff from the old PR. Ignore.

Copy link
Author

Choose a reason for hiding this comment

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

Diff from the old PR, ignore. I haven't at all touched the tests for this PoC.

Comment on lines +43 to +47
if (initialState && 'then' in initialState) {
// TODO: If we want to preserve backwards compatibility, we'd need to throw here instead
// @ts-expect-error See above
return React.use(initialState);
}
Copy link
Author

Choose a reason for hiding this comment

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

If initialState is passed in as a Promise, this is going to make sure any hook that relies on it suspends at the location of the hook. Before, you would always have to await at the provider level.

This should not be a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

Does this mean we are requiring React@19? I believe use() is not available in 18

Copy link
Author

@Ephem Ephem Nov 20, 2025

Choose a reason for hiding this comment

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

The comment mentions this but is not super clear, but taking this all the way would mean we refactor this to throw the promise instead for backwards compatibility. That's a bit more work to implement though so went with use just for the poc. 😄

If we want to use use and avoid introducing a legacy React way to do this, maybe we could instead error if passing in initialState as a Promise on React < 19?

Edit: Since we own the Next package that will likely be the first and for a while the only user of this, we could either await initialState or pass down the promise directly depending on the Next version. Pre 16 we await it like today, >16 we pass down the promise.


const propsWithEnvs = mergeNextClerkPropsWithEnv({
...rest,
initialState: statePromiseOrValue as InitialState | Promise<InitialState> | undefined,
Copy link
Author

Choose a reason for hiding this comment

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

Not awaiting this at the top level might be considered a breaking change. I'm not sure it is technically, but it might change the loading experience of apps, so we should be careful.

We could also do this incrementally and optionally by introducing something like dynamic="stream".

// which for example means that already mounted <Suspense> boundaries might suddenly show their fallback.
// This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's
// called during a transition, it immediately uses the new value without deferring.
return useDeferredValue(authState);
Copy link
Author

Choose a reason for hiding this comment

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

This is a common pattern for all the hooks and is a breaking change. Doing this by default in the react package might introduce transitions into apps that don't have them currently, which might cause problems, so we'll likely want to find a way to introduce this incrementally/optionally and let the framework level decide.

This does not by itself get rid of the transitive state. To do that safely we probably have to start the transition ourselves from inside clerk-js so we can make sure it wraps both the navigation and the state emit.

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.

4 participants