Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(react): deprecate SignOutButton signOutOptions prop by jacekradko · Pull Request #8147 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-signout-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/react": patch
---

Deprecate the `signOutOptions` prop on `<SignOutButton />` in favor of top-level `redirectUrl` and `sessionId` props. The `signOutOptions` prop still works but now emits a deprecation warning.
18 changes: 16 additions & 2 deletions packages/react/src/components/SignOutButton.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { deprecated } from '@clerk/shared/deprecated';
import type { SignOutOptions } from '@clerk/shared/types';
import React from 'react';

Expand All@@ -7,18 +8,31 @@ import { withClerk } from './withClerk';

export type SignOutButtonProps = {
redirectUrl?: string;
sessionId?: string;
/**
* @deprecated Use the `redirectUrl` and `sessionId` props directly instead.
*/
signOutOptions?: SignOutOptions;
children?: React.ReactNode;
};

export const SignOutButton = withClerk(
({ clerk, children, ...props }: React.PropsWithChildren<WithClerkProp<SignOutButtonProps>>) => {
const { redirectUrl = '/', signOutOptions, getContainer, component, ...rest } = props;
const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;

if (signOutOptions) {
deprecated('SignOutButton `signOutOptions`', 'Use the `redirectUrl` and `sessionId` props directly instead.');
}

children = normalizeWithDefaultValue(children, 'Sign out');
const child = assertSingleChild(children)('SignOutButton');

const clickHandler = () => clerk.signOut({ redirectUrl, ...signOutOptions });
const clickHandler = () =>
clerk.signOut({
redirectUrl,
...(sessionId !== undefined && { sessionId }),
...signOutOptions,
});
Comment on lines +21 to +35

@coderabbitaicoderabbitaiBotMar 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Top-level props lose precedence when both APIs are provided (migration bug)

At Line 34, ...signOutOptions is spread after Line 32-33, so deprecated nested values override top-level redirectUrl/sessionId. That keeps the ambiguous behavior this deprecation is meant to eliminate and can sign out/redirect using the wrong target when consumers pass both during migration.

Proposed fix (preserve backward compatibility, make top-level authoritative)
- const { redirectUrl = '/', sessionId, signOutOptions, getContainer, component, ...rest } = props;+ const { redirectUrl, sessionId, signOutOptions, getContainer, component, ...rest } = props;++ const resolvedRedirectUrl = redirectUrl ?? signOutOptions?.redirectUrl ?? '/';+ const resolvedSessionId = sessionId ?? signOutOptions?.sessionId;
@@
const clickHandler = () =>
clerk.signOut({
- redirectUrl,- ...(sessionId !== undefined && { sessionId }),- ...signOutOptions,+ redirectUrl: resolvedRedirectUrl,+ ...(resolvedSessionId !== undefined && { sessionId: resolvedSessionId }),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/SignOutButton.tsx` around lines 21 - 35, The
clickHandler in SignOutButton currently spreads signOutOptions after the
explicit redirectUrl/sessionId so nested values can overwrite top-level props;
change the argument to clerk.signOut so signOutOptions is spread first and then
explicit redirectUrl and sessionId are applied (i.e., spread signOutOptions
before adding redirectUrl and the conditional sessionId) to ensure top-level
redirectUrl/sessionId on the SignOutButton take precedence over deprecated
signOutOptions.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This is the existing functionality and we want to maintain it until the next major version. Keeping this unchanged is correct

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jacekradko, understood — keeping ...signOutOptions spread last preserves the existing override behavior for consumers who currently rely on it, and the deprecation warning is sufficient signal until the next major version. Makes sense, thanks for the clarification!


✏️ Learnings added
Learnt from: jacekradko
Repo: clerk/javascript PR: 8147
File: packages/react/src/components/SignOutButton.tsx:21-35
Timestamp: 2026-03-23T16:09:20.099Z
Learning: In `packages/react/src/components/SignOutButton.tsx` (Clerk React), the `SignOutButton` component intentionally spreads `signOutOptions` last in the `clerk.signOut(...)` call (after top-level `redirectUrl` and `sessionId`), so that `signOutOptions` values override the top-level props. This backward-compatible behavior is preserved until the next major version, even though `signOutOptions` is deprecated. Do not flag this spread order as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
return clickHandler();
Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/__tests__/SignOutButton.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,18 @@ describe('<SignOutButton />', () => {
});
});

it('handles sessionId prop', async () => {
render(<SignOutButton sessionId='sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' />);
const btn = screen.getByText('Sign out');
await userEvent.click(btn);
await waitFor(() => {
expect(mockSignOut).toHaveBeenCalledWith({
redirectUrl: '/',
sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe',
});
});
});

it('handles signOutOptions prop', async () => {
render(<SignOutButton signOutOptions={{ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }} />);
const btn = screen.getByText('Sign out');
Expand Down
Loading