useParam() doesn't return a parameter if the value of it starts with a hash while using the HashRouter
Describe the bug
I have this route setup currentlly:
<HashRouter>
<Route path="/:roomId?" component={App} matchFilters={{
roomId: /^(!|\$|#)/ // Match $ or # for the roomId
}} />
<Route path="/login" component={Login} />
</HashRouter>
Using http://localhost:5173/#/$test works, using http://localhost:5173/#/!test works but using http://localhost:5173/#/#test causes the roomId param to be undefined when using it.
Example usage is:
export default function App() {
const params = useParams();
return (
<ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
<div class="wrapper">
<Show when={params.roomId} fallback={<p>No Room</p>} keyed>
<p>room</p>
</Show>
</div>
</ErrorBoundary>
)
}
Your Example Website or App
See the above for a minimal example of the issue.
Steps to Reproduce the Bug or Issue
See above for a minimal example of the issue and how to reproduce it please.
Expected behavior
I would expect that #test is equally valid as any other string and isnt omitted.
Screenshots or Videos
No response
Platform
- OS: Linux Fedora 40
- Browser: Firefox 128.0 (64-Bit)
- Version: SolidJs ^1.8.18 | Solidjs-Router ^0.14.1
Additional context
I haven't checked the other routers as my app isnt SSR compatible
After reviewing the source code, I realized that I shouldn't make any modifications to address this issue.
I summarized the problem as follows:
Analysis: Why Modifying HashRouter's Core Parser is Not Recommended
Bug Summary
The issue occurs when using # as the first character in route parameters (e.g., /#/#test). The hashParser function treats these as hash anchors instead of route parameters, causing params.roomId to be undefined.
Technical Root Cause
export function hashParser(str: string) {
const to = str.replace(/^.*?#/, "");
if (!to.startsWith("/")) {
const [, path = "/"] = window.location.hash.split("#", 2);
return `${path}#${to}`; // Treats as hash anchor
}
return to;
}
For URL http://localhost:5173/#/#test:
str.replace(/^.*?#/, "")returns#test- Since
#testdoesn't start with/, the condition is true window.location.hash.split("#", 2)creates["", "/", "test"], path becomes/- Returns
/#test(interpreted as hash anchor, not route parameter)
Why Library Modification is Not Recommended
1. Browser History Navigation Issues
Problem: History Stack Inconsistency
// User navigation sequence
User visits: /#/#test
Browser stores: /#/#test
Parser converts: /#test (as hash anchor)
// On browser back/forward
Browser restores: /#/#test
Parser re-runs: /#test (hash anchor again)
Result: params.roomId = undefined
Concrete Example:
Navigation flow:
/#/!room1 → /#/#room2 → /#/$room3 → back → back
Expected:
/#/$room3 → /#/#room2 → /#/!room1
Actual (with modified parser):
/#/$room3 → undefined → /#/!room1
// #room2 fails because it's treated as hash anchor
2. Conflict with Native Browser APIs
History API Mismatch:
// HashRouter's set function stores actual URL
set({ value, replace, scroll, state }) {
window.history.pushState(state, "", "#" + value); // Stores /#/#test
}
// But parser returns different value
hashParser("/#/#test") // Returns /#test
// Creating inconsistency between stored and parsed values
3. Hash Anchor Functionality Collision
Unintended Scrolling:
// HashRouter tries to scroll to element
const hashIndex = value.indexOf("#");
const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
scrollToHash(hash, scroll); // Searches for element with id "room2"
4. Performance Overhead
Every Hash Change Requires Pattern Matching:
// Modified parser would need regex on every navigation
export function hashParser(str: string) {
const to = str.replace(/^.*?#/, "");
if (!to.startsWith("/")) {
if (to.match(/^[!$#]/)) { // Performance cost on every hash change
// Special handling
}
}
}
5. Maintainability Concerns
- Library Updates: Custom modifications lost during updates
- Debugging Complexity: Unexpected behavior confuses developers
- Team Development: Other developers unaware of custom modifications
- Side Effects: Unpredictable interactions with other routing features
Recommended Solutions
Option 1: URL Encoding (Transparent to Users)
// Helper functions
export const createRoomUrl = (roomId) => {
const encoded = roomId.startsWith('#')
? encodeURIComponent(roomId)
: roomId;
return `/#/${encoded}`;
};
export const parseRoomId = (param) => {
return param ? decodeURIComponent(param) : undefined;
};
// Usage
navigate(createRoomUrl("#test")); // Results in /#/%23test
const roomId = parseRoomId(params.roomId); // Returns "#test"
Option 2: Update Route Matcher
<Route
path="/:roomId?"
component={App}
matchFilters={{
roomId: /^(!|\$|%23)/ // %23 = URL-encoded #
}}
/>
Option 3: Documentation and User Education
Clearly document supported URL patterns:
✅ Supported:
http://localhost:5173/#/!test
http://localhost:5173/#/$test
http://localhost:5173/#/%23test (URL-encoded #)
❌ Not supported:
http://localhost:5173/#/#test (conflicts with hash anchors)
Conclusion
While technically possible to modify the hashParser, it introduces significant risks around browser history navigation, performance, and maintainability. The recommended approach is to use URL encoding with helper functions, which provides a clean API while maintaining compatibility with the router's core functionality.
The library's current behavior is by design to maintain compatibility with standard web navigation patterns, and modifying core parsing logic should be avoided to prevent unpredictable side effects.