htnx: a htmx-like experience in Next.js (seriously)
Okay, I did it; this probably broke every rule or principle that htmx has, and it's hard to defend myself… So why did I do this? For fun! Guess what? Coding can be fun, and doing things can be entertaining.
IMPORTANT:
This was created mainly as a joke, and I don't even know if it's stable and reliable enough to be used in production, so please don't do that. If you want to use htmx, use htmx, it's a great project, and I'm sure it's way more reliable than this.
#Backstory
Over the past months I've heard a lot about htmx and decided to look at it, and it feels magical. In a sea of complexity and maximalism, something as simple as HTML on... steroids? It sounds like a fantastic new world.
And after that, of course, I had to try it, and it's awesome and a really good experience, but while reading about it, me being a React kind of guy (outch) couldn't stop thinking about RSC, and how strangely enough in my mind those things could work really well together (yes, I get scared about my own thoughts sometimes too). So guess what?
#Let's try this!
I never actually read about returning components from React Server Actions on Next.js, but it definitely is possible, as someone pointed out on Twitter some months prior (didn't find the tweet), so what's then? What else do we need to do?
#Structure
To be usable, those things cannot only return something; what do you do with that then? Append the document, console.log
em? Not sure about that last one.
#We need to "swap!"
swap
is a property described by the htmx docs as:
"htmx offers a few different ways to swap the HTML returned into the DOM. By default, the content replaces the innerHTML of the target element."
innerHTML
outerHTML
So, these are some of the possible values.
We can achieve this with conditionals because in React Land we don't believe in changing the DOM; the invisible framework hand does that, so here, with some conditionals and sweet JS syntax sugar, we can achieve these properties. Here's how it looks:
Here I took a kind of artistic liberty and decided to make the default value of
swap
to benone
so you can just call actions without having to worry about it.
That's cool and all, but then, when something gets called or invoked?
#Enters "trigger!"
As the htmx docs say:
"By default, AJAX requests are triggered by the “natural” event of an element"
This beeing:
- input, textarea & select are triggered on the change event
- form is triggered on the submit event
- everything else is triggered by the click event
This can be simply achieved for us with a variable event object spread on the final createObject
prop, like this:
This is what will make your markup do the things you expect. But now, what if we need something else to change?
#"target" is here!
"If you want the response to be loaded into a different element other than the one that made the request, you can use the hx-target
attribute, which takes a CSS selector."
This is where we take a bit of a different path, and instead of using a CSS selector, we use refs to target the element we want to change. This is more of a "React" way of doing things, and it's also more reliable, so here's how it looks:
Ps. the renderToStaticMarkup
is a bit of an unusual choice, but it's the only way I found to render a component to a string on the client side. If you know a better way, please let me know!
Just to point out, the "CSS selector" style can be achieved in React, but this one was too dirty even for me in this project.
#Overall structure
After that, I think we can call it a usable htmx like prototype. Now let's just give it the final touches and make the experience a bit more enjoyable.
#Indicators
With the advent of React 18 and the new hooks to deal with Server Actions, we now have useTransition
a super simple way to deal with loading states while we wait for the server to respond, so we can now have a loading indicator for our actions.
With these simple tweaks, we can now call actionFn(...)
instead of action(...)
and as simple as that, now we have a loading state, and we can show any arbitrary component we want; in this case, we expose the indicator prop, so you can pass any component you want to show while the action is pending.
#Types!
This is a bit of a tricky one; there were two routes to go with the API: we could have HTNX
as an object and use it like framer-motion
does, with HTMX.button
and go on. but this here would cause too much code duplication.
So I decided to use a bit more of what React gives us from their API and use types as guardrails; that way, we have a unique component that is shaped by the decisions you make. Want a button? Just pass element={"button"}
and the types will adapt to that. Here's a bit of how this typing looks:
-
Swap is pretty self-explanatory; it's the same as the htmx docs.
-
Trigger on the other hand, is a bit more complex. We have a base type that is
click
andmouseenter
because pretty much everything can be triggered by those two, but for forms, we havesubmit
which is pretty unique, so we can only let it be an option for forms. -
Event is a bit of a tricky one; we need to know what kind of event we are dealing with so we can have the correct type for the event. For forms, we have
FormData
and for everything else, we haveSyntheticEvent
which is not the best possible type to describe all but is generic enough to work for most cases.
All of those are based mainly on ReactHTML
and ComponentProps
which provide great types for the HTML elements and can be extended to fit our needs and index each other.
Overall, we have a bit of trickery around the component, just to make everything fit in nicely, but that's the gist of it.
#Usage
Now that we have everything in place, let's see how it looks in practice.
And just to scratch that itch on every React developer who hasn't seen how to use server action to return React components, here's how those look:
Aren't those beautiful? I think they are.
#Conclusion
Okay so... This was a lenthly one, but I think it was worth it. I had a lot of fun doing this, and I think it's a pretty good example of how you can use React to do some pretty cool stuff. Not everything needs to be production-ready, and not everything needs to be perfect. Sometimes you just need to have fun and do things.
NOTE:
I don't know if I mentioned it, but I deployed a live version of this demo, and I know I'm biased, but it's looking sick! Check it out!
I hope you enjoyed this one, and if you have any questions or suggestions, please let me know on Twitter @rafaelrcamargo or on the htnx repo.