feat(refs): add createRef primitive, the ultimate solution #807
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
First of all, thank you for making Solid - I really love it and use it in my projects. It's truly the framework that makes sense.
I've been using
createRef
in my SolidStart projects and I love it. I use it to create components etc, it's very productive and performatic.Note: I used AI to help structure and improve the writing (English isn't my first language), but all ideas, code, and examples are mine from real production use.
The Problem: Wrappers Without Purpose
Looking at the current primitives landscape, I noticed a concerning pattern. Many primitives are essentially wrappers around APIs that already work perfectly:
createTimer
(375B) - wrapssetTimeout
/setInterval
createGeolocation
(465B) - wrapsnavigator.geolocation
createScrollPosition
(745B) - wrapselement.scrollTop
createResizeObserver
(611B) - wraps nativeResizeObserver
createEventListener
(372B) - wrapsaddEventListener
createFullscreen
(411B) - wrapsrequestFullscreen()
createClipboard
(499B) - wrapsnavigator.clipboard
createPageVisibility
(406B) - wrapsdocument.visibilityState
These don't solve real problems - they just add overhead to APIs that already work.
The philosophy says "Wrap base level Browser APIs" but I believe this should mean "solve real problems that native APIs can't handle elegantly" - not create abstractions for the sake of it.
The Real Problem: SSR + Element Existence
Here's a problem that actually needs solving:
This is the real problem: defensive programming everywhere, SSR checks, element existence checks, manual cleanup, hydration mismatches.
The createRef Solution
This is not just another wrapper - it's a formal contract that guarantees your DOM code only runs when elements actually exist, client-side, with automatic cleanup.
Why This Changes Everything
🛡️ Formal Contract, Not Just Convenience
addEventOnChange
isn't reactive - it's a guarantee. Your callback only runs when the element definitively exists. No checks needed.🚫 SSR-Safe by Design
Client-only execution eliminates hydration mismatches. No more
typeof window !== 'undefined'
checks.🎯 Non-Reactive Explicit Control
Unlike signals, you have explicit control. Call
element()
anywhere - it won't trigger side effects unless you explicitly add them.🧹 Automatic Cleanup
Return functions from callbacks for safe teardown. Everything gets cleaned up automatically.
Foundation for Better Primitives
createRef
isn't about replacing existing primitives - it's about giving them a bulletproof foundation. Every primitive solving SSR safety from scratch creates unnecessary overhead and complexity.Enhanced Event Listeners
Shared Ref Architecture
Benefits of Shared Foundation
🛡️ Eliminates SSR Boilerplate
No more
onMount()
wrappers in every primitive. The foundation handles element existence.⚡ Zero Overhead Composition
Multiple primitives share one ref system instead of each creating their own.
🎯 Consistent API
All primitives follow the same "element existence guarantee" pattern.
🔧 Extensible
Developers can add custom logic alongside existing primitives using the same safe foundation.
Migration Path
Existing primitives keep their APIs, but internally use
createRef
:The result: Better DX, zero breaking changes, shared robust foundation.
Real Example: Complex Orchestration That Makes Sense
This is valuable because it:
createRef
as foundation for guaranteed executionCompare:
createScrollPosition
=element.scrollTop
(trivial wrapper)createPagedScroll
= complex orchestration solving real UX problemAPI Surface
setter
<div ref={setter} />
).element()
addEventOnChange(cb, key?, once?)
cb
when ref mounts/changes (explicit control).removeEventOnChange(key)
cleanup()
RefRef(a, b)
The Bottom Line
Most current primitives are wrappers without purpose. They add overhead to APIs that already work perfectly.
createRef
is different. It's the formal contract that solves the fundamental problem of SSR-safe, guaranteed DOM interactions.It's not just another primitive - it's the foundation that other primitives should build on.
Todos
packages/refs/src/createRef.ts
@solid-primitives/refs