Skip to content

Conversation

@drewzhao
Copy link

Problem

HashRouter navigation is broken in Stoplight Elements v9.0+ when used in web component contexts (Custom Elements) or when embedded in other SPA frameworks like VitePress.

Symptoms:

  • ✅ URL hash updates correctly when clicking navigation links (e.g., #/operations/someOperation)
  • ❌ Page content does NOT refresh to show the selected endpoint/schema
  • ❌ Browser back/forward buttons don't work
  • ❌ Makes API documentation completely unusable for navigation

Root Cause:
This is a regression introduced in v9.0.0 when Elements upgraded from React Router v5 to v6 (commit 8520585). React Router v6's HashRouter doesn't properly respond to hash changes when:

  1. Running inside web components with Shadow DOM
  2. Event bubbling works differently at web component boundaries
  3. Embedded in another SPA framework with its own router

Impact:
Affects all users deploying Elements as web components with router="hash", forcing them to either:

  • Stay on v8.5.2 (missing security updates and new features)
  • Switch to history router (requires server configuration)
  • Implement custom workarounds

Solution

This PR adds a HashRouterSync component that bridges browser hash changes with React Router v6's navigation system.

Implementation:

  1. Created packages/elements-core/src/components/HashRouterSync/index.tsx

    • Listens for hashchange and popstate events from the browser
    • Uses React Router's useNavigate() hook to programmatically navigate
    • Syncs window.location.hash with React Router's internal state
    • Prevents infinite loops through careful state tracking
  2. Modified packages/elements-core/src/hoc/withRouter.tsx

    • Conditionally renders HashRouterSync when routerType === 'hash'
    • Follows same pattern as existing ScrollToHashElement component
    • Zero impact when using router="history" or router="memory"
  3. Exported from packages/elements-core/src/index.ts for public use

Benefits:

  • Zero Breaking Changes - Completely backward compatible
  • Opt-in by Design - Only activates when router="hash"
  • Minimal Code - 58 lines, 3 files modified
  • Clean Architecture - Follows existing patterns
  • Framework Agnostic - Works in any embedding context

Testing

Automated Tests:
Validated in VitePress environment with Playwright:

✅ 4/4 tests passed
✅ Navigation to different schemas updates both URL and content
✅ Browser back/forward buttons work correctly
✅ Multiple sequential navigations work
✅ No console errors

Manual Testing:

  • Tested in VitePress with hash router ✅
  • Tested in plain HTML with web components ✅
  • Verified v8.5.2 behavior is restored ✅
  • Confirmed no impact on history router ✅

Unit Tests:

  • Added comprehensive tests for HashRouterSync component following Testing Library principles
  • Tests verify behavior (not implementation) as per contributing guidelines
  • Used React Testing Library and jest-dom assertions
  • 14 tests, all passing - Hash changes, popstate events, mount sync, edge cases
  • Full test suite: 354 tests passed - No regressions introduced
  • TypeScript compilation: No errors - All type checks pass

Files Changed

packages/elements-core/src/components/HashRouterSync/index.tsx (NEW - 58 lines)
packages/elements-core/src/components/HashRouterSync/__tests__/HashRouterSync.spec.tsx (NEW - 450 lines)
packages/elements-core/src/hoc/withRouter.tsx (MODIFIED - 12 lines)
packages/elements-core/src/index.ts (MODIFIED - 1 line)

Total: ~520 insertions, 3 deletions

Versioning

Following the versioning guidelines:

  • elements-core: Patch bump (bug fix)
  • elements: Patch bump (depends on elements-core fix)
  • elements-dev-portal: Patch bump (depends on elements-core fix)

Checklist

  • Fix implemented following existing code patterns
  • Tested in real-world integration scenarios
  • No breaking changes
  • Follows TypeScript best practices
  • Unit tests added following Testing Library principles (14 tests, all passing)
  • Full test suite passes (354 tests)
  • TypeScript compilation verified (no errors)
  • Documentation updated (code comments)
  • Works with existing routers (hash, history, memory)

Related

This fix resolves a critical bug #2792 affecting all v9.x users deploying Elements as web components with hash routing. The issue was introduced during the React Router v5→v6 migration and has made Elements v9.x unusable for this common deployment pattern.


Note: This is my first contribution to Stoplight Elements. I'm happy to make any changes requested by the review team. Comprehensive unit tests have been added following the project's testing guidelines.

React Router v6's HashRouter doesn't properly respond to hash changes when
used in web component contexts (Custom Elements with Shadow DOM) or when
embedded in other SPA frameworks like VitePress.

This commit adds a HashRouterSync component that:
- Listens for hash change and popstate events
- Forces React Router to navigate when the browser hash changes
- Ensures content updates when users click navigation links
- Only activates when router type is 'hash'

The fix is transparent to users and requires no API changes.

Fixes navigation issues reported with Elements v9.0.12 after the React
Router v5 to v6 upgrade (commit 8520585).

Technical details:
- Created HashRouterSync component using useNavigate and useLocation hooks
- Integrated into withRouter HOC's InternalRoutes
- Syncs on mount, hashchange, and popstate events
- Prevents infinite loops by tracking current hash state

Testing: This fix has been validated in VitePress environments with
hash-based navigation, confirming that:
- URL updates correctly on navigation
- Content refreshes when clicking navigation links
- Browser back/forward buttons work properly
- No performance impact or console errors
@drewzhao drewzhao requested a review from a team as a code owner November 19, 2025 17:20
@netlify
Copy link

netlify bot commented Nov 19, 2025

Deploy Preview for stoplight-elements ready!

Name Link
🔨 Latest commit 99d2f54
🔍 Latest deploy log https://app.netlify.com/projects/stoplight-elements/deploys/691dfc4f64967100087a9f1d
😎 Deploy Preview https://deploy-preview-2850--stoplight-elements.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 19, 2025

Deploy Preview for stoplight-elements-demo ready!

Name Link
🔨 Latest commit 99d2f54
🔍 Latest deploy log https://app.netlify.com/projects/stoplight-elements-demo/deploys/691dfc4f8c4be700077d4bd1
😎 Deploy Preview https://deploy-preview-2850--stoplight-elements-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant