This morning I had an interesting chat with @jlarky on Twitter sparked by this question of his:
do refs work in react server components? use case:
<div id="hideme"> <button onClick={() => window.hideme.remove()} /> </div>
and I want to make the button a client component and div a server component
The answer is no — refs don't work in Server Components.
Why not?
On the surface the answer is simple: refs (the ones used for elements) are references to DOM nodes. And DOM nodes don't exist on the server, since the DOM doesn't exist on the server. The DOM is a reference to the browser's in-memory instantiation of the HTML tree. So, code that's running on a different computer (i.e. Node.js on the server) just doesn't have access to that, by its very nature.
But there's a deeper reason why.
The reality is React could have chosen to bless something like the code snippet above. Perhaps it could take all the onClick
handlers, stringify them on the server, and eval
them on the client. (Set aside for the moment cases where the handler closes over some server state.) What would React be giving up if it allowed this?
First, because the reference to hideme
is via a string, we'd lose the guarantees of JavaScript lexical scope. A simple typo would break this code, but nothing in JavaScript would be able to point this out to us:
<div id="hidemee"> {/* whoops! */}
<button onClick={() => window.hideme.remove()} />
</div>
When we use refs in client components, we're working with JavaScript variables, which give us the full power and feedback of the language:
let containerRef = useRef();
<div ref={containerReff}> {/* Error: containerReff is not defined */}
<button onClick={() => containerRef.current?.remove()} />
</div>
Lexical scope also ensures our components don't break composition, that is, that our code doesn't trigger any "spooky action at a distance." In the first example, there's nothing stopping someone from coming along later and adding a second div with the same ID:
<div id="hideme">
<button onClick={() => window.hideme.remove()} />
</div>
{/* ... */}
<div id="hideme">
<p>New section</p>
</div>
Second, React wants to know which parts of our tree are interactive, so it can give us guarantees about how our UIs will behave – for example, that different parts of the DOM that represent the same state will always be in agreement.
Showing and hiding content in React is handled via state and a conditional:
const [isShowing, setIsShowing] = useState(true);
{isShowing && (
<div>
<button onClick={() => setIsShowing(false)} />
</div>
)}
This lets React preserve the element's visibility if the component re-renders due to other state updates, and pure re-renders are what help us avoid the kind of UI inconsistencies that led to React being created in the first place.
Now at times, these constraints can feel frustrating. Compared to the original snippet, having to create a client component just to use a ref for a bit of interactivity seems onerous. It makes me think of other constraints – the implicit ones known as the "Rules of React", like avoiding reading or writing refs during render, that JavaScript as a language can't help us spot at all. They won't cause a syntax error, or fail our build (though tools like ESLint and the React Compiler can provide extra guidance). But all these rules and constraints can leave developers feeling frustrated that they can't just do the simple thing they're trying to do.
@jlarky writes:
Oh but I do this from time to time :-( it does make React unhappy, but I have my ways ;)
and
I agree. In terms that sometimes you have to give up on some of those guarantees
I strongly believe that the best way to use a library or framework is to get in the head of its creator. You have to understand the problems that the tool was designed to solve, and decide whether you are facing those same problems, to get the most out of it. If you don't, you'll just end up in a frustrating cycle of framework-fighting.
And if React's creators had decided to build it using a bespoke programming language, they could have baked all its rules and constraints into its actual syntax. The language could have taken on much more of the burden of guiding developers along the happy path.
But React is written in JavaScript. That decision has made it more accessible to web developers everywhere. But it also means that to be successful with it, and enjoy it, developers must learn about the additional constraints it imposes on their code – both how to follow them, as well as the benefits those constraints afford.
When I started thinking of React as a programming language for UIs, and of its rules as its syntax, I became more happy and productive with it than any other tool I had used. I'd encourage you to start thinking about it in the same way, too.