Regretting React: Reflections on modern tech for hobby projects
Hi, I’m Matchu, and I run a Neopets site that’s been going for over ten years!
For most of that time, it’s been a Ruby on Rails app, but a few years ago I got real fatigued about it. I’d let things rot, and making real changes felt impossible. I’d just tweak a line of code in production, restart the app, and pray.
But in 2020, we needed big changes! Our site was very dependent on Flash, which had finally been blasted off the earth—and Neopets was moving to a new HTML5 animation solution, but we hadn’t kept up.
So, feeling powerless about my old app, and feeling optimistic about React, I decided it was time for a ✨ rewrite ✨! We were gonna abandon old technologies, replace our Flash dependence, add support for mobile, and make everything beautiful!
Here’s the spoiler: I spent most of 2021 reimplementing the app in Next.js, and burned out a few major features before completion, oops. So now we have two weird incomplete copies of the app floating around, neither of which is in a realistic state to invite new teammates into.
I expected React to be the modern tech that would fix everything, especially having delightedly used it professionally for so many years… but here’s what I missed:
The React ecosystem is designed for large teams writing complex apps, and it makes the exact wrong trade-offs for hobby developers like me.
(Also, one little note before we get into it: you should code in whatever you want!! Everyone’s goals are different, and the act of creation is sacred. For me, I’m just reflecting on how React hasn’t served my own creative goal of long-term survival on a near-zero budget—but hobby software is inherently art, and your art is about you, and it should be literally whatever you want it to be.)
Components enable and encourage big complexity
React is all about building tons of components to wrangle your complexity: if you build good component boundaries, and stick to them, you can trust that adding new things probably won’t break other things.
But, pause. Do you see the subtle bit in there? The one and only promise made?
In a highly componentized system, the simplest and safest operation is to add things.
React is built by and for tech giants, and their needs are unique: they want features shipped ASAP to meet this quarter’s OKRs or else get fired, and their teams want to talk with each other as little as possible. Componentizing everything helps a lot with that!
But if you’re not a tech giant, hyper-abstracting your project can send it spiraling out of control.
In my case, I found Next.js incredibly empowering at the start: just slap in new components, create new API endpoints, any given feature is really fast to build—and it’s surprisingly easy to make extremely rich and impressive UI interactions.
But now, the new app is quite shiny, but also it’s huge—way more than is reasonable for the three core things it actually does. I wish I’d made it simpler and duller, because that’s more reliable for users, more maintainable for me, and more welcoming for new contributors. (Heck, hobby users are used to some measure of jank—you don’t have to be Facebook in order to compete!)
Hobbyists need to build aggressively simple software if we intend to maintain it long-term. We don’t have Silicon Valley’s funding, so if we try to keep up with their UI bar, we’re setting ourselves up for burnout. Simple apps stay alive, but component systems like React actively make feature-creep the most natural thing to do.
And like… of course there are ways to use React to push back against these forces. I could have used it differently and gotten a different result. But avoiding these mistakes would’ve required distrusting and fighting against my own tool—because it’s a tool designed primarily for the best-funded software teams in the world, not for me.
Component frameworks have unusually high lock-in
In my component-mania, I ended up adopting Chakra UI, a very impressive piece of technology with a lot of built-in components for complex cases like tooltips and modals! It also encourages you to use their Tailwind-like components for basic styling too, with an impressive theming engine.
Here’s a code sample from their site:
<Center h="100vh">
<Box p="5" maxW="320px" borderWidth="1px">
<Text mt={2} fontSize="xl" fontWeight="semibold" lineHeight="short">
Modern, Chic Penthouse with Mountain, City & Sea Views
</Text>
<Text mt={2}>$119/night</Text>
<Flex mt={2} align="center">
<Box as={MdStar} color="orange.400" />
<Text ml={1} fontSize="sm">
<b>4.84</b> (190)
</Text>
</Flex>
</Box>
</Center>
Like Tailwind, it’s easy to build things quickly, and it works pretty well with small components where splitting appearance from behavior can make things feel harder to understand instead of easier…
…but ultimately, Chakra UI is one of the decisions I regret most for the new app, because none of the UI I wrote is portable. It all has to go in the trash.
If I had used something more like CSS Modules, or heck even Tailwind!, then I’d be able to pull major UI elements pretty straightforwardly into a non-React system. That’s not to say it would be trivial, but I’d still be able to copy major HTML elements, and nearly the entire stylesheet, almost anywhere else. But if I want to eject this UI from React, I’m starting much closer to zero.
Using nonstandard tech also made it much harder to bring in new teammates: if we were using plain-ish HTML/CSS in our React components, then we’d be able to lean on a lot more of their existing knowledge. But with a UI framework, they need to learn a whole new styling system before they can even begin to read what’s going on, much less write.
And this also kept the hobbyist community from being able
to remix my work! Next.js output is awkward for modders in
general, but it gets even worse when styling systems output obtuse
and unstable classes like
.css-f2d01
. Modders can’t write browser extensions or scrapers, because
there’s no semantic markers left in the code to identify what
something is. My tech is actively excluding fellow coders
from participating in our community, right now.
In short, I’m coming to realize that every time I chose nonstandard technologies, it kept me working on my projects alone.
Next.js offers very little, for better and then worse
It’s kinda freeing to walk into Next.js and not have to ask yourself “how do I fit this into the framework”? If I want a new UI element, I drop it in. If I want a new server-side behavior, I add an endpoint.
But as I keep having to homegrow more and more infrastructure to provide the basic functionality I’m used to with Rails and similar frameworks… well, what initially felt simple now feels complex.
I keep hacking in just a little bit more infra, just a liiiittle bit more, gradually creating the strange not-quite-standard thing we have now. It’s lean, but always just a little bit bad, and the gaps between this vs an intentionally-designed developer experience are starting to show.
It’s easy to find any infra you want on npm, of course… but is it from an author you can trust, and does it slot cleanly into the rest of your architecture? That’s a much taller order, and I often found myself choosing homegrown solutions over libraries I didn’t feel I could entrust sensitive user data to.
They’re both bad options, and I still think homegrown was the right decision between the two… but extensive custom infra is a cost I didn’t anticipate when choosing Next.js, and it’s a significant mark against it for me now.
Tech startups generally have teams of specialists managing their React infrastructure. Hobbyists can’t do what startups do, but that’s the customer Next.js is designed for.
Okay, how do we come back from that?
So, here we are, with two live apps: an ancient Rails app that’s missing key features and impossible to run on modern systems, and a “modern” Next.js app that’s already hard to manage and keeps running out of memory for reasons unknown. What now?
Originally my plan had been to buckle down and finish the Next.js app… but I just don’t trust React with this project anymore. Neopets is getting old, and lost media concerns are becoming relevant, and I don’t think React is stable enough to last the next ten years—not without a huge ongoing investment for something that truly isn’t complex enough to require this much maintenance.
But I also know better than to just Build A Third Site lmao, two competing sites is already enough.
So, we’ve started on a new solution to get to one modern site without throwing away our work: merge what we’ve already got!
- I’ve revitalized the Rails app all the way up to the latest version, and deleted a lot of unused cruft along the way.
- I’ve dropped the most important parts of our new React UI into the app as-is, to bring in the most important new features (like the HTML5 animation support), without committing to the entire React ecosystem anymore.
- And I’m keeping the Next.js app online to serve as the backend server for those components… but once we finish the hybrid, we should be able to stop pointing people to the Next.js site directly; and then we can begin to migrate those backend responsibilities back into the Rails app too, to eventually tear down the Next.js app altogether.
I feel pretty good about moving back to Rails, considering at how stable it’s been in the years since. I love how many batteries are included if you stick to a relatively simple app (which I need to do for maintenance’s sake anyway!), and it has so many developer experience affordances, and it seems equipped to stick around for a long time without too much churn.
(Contrast this to Next.js, which announced at a keynote last year that they’re overturning their entire pages architecture!)
There’s things I worry about deeply in Rails’s governance: DHH is such a concerning person, and he seems to exert pretty total control?? But from the field of options (esp. options that we’ve already written ~90% of the site in lol), it feels like one of the better choices for hobbyists like me.
I’m wary about changing the plan again? I hope I’m not just someone who can never be satisfied. But also like… I really do think the app simply isn’t capable of serving as lost media protection as-is, and I really am serious about that goal. It’s functional today, and I could just walk away and let that be that? But I also want this app to be functional in ten years, and right now it simply won’t be. So, 🤞❗️
Advice for those on this same journey
The main insight that’s helped me here isn’t “Rails good, React bad”. Rather: If I intend for a hobby project to stand the test of time, I need to use aggressively simple and boring technologies.
- I need to be rejecting UI designs that substantially diverge from what the browser already provides.
- I need to be rejecting intricate performance upgrades to avoid 200ms of load time.
- I need to be using the most bog-standard tooling possible, and avoiding service dependencies altogether when I can.
And in my analysis, the standard usage of Next.js and React fail all of the above:
- The React ecosystem deeply encourages nonstandard UI. (Powerful! But expensive!)
- Next.js is entirely architected around minimizing page speed metrics, mainly to win SEO battles—and those trade-offs are baked deep into its architecture, including the decision to offer very few infrastructural affordances.
- React simply doesn’t have boringly standard tooling yet. Everything is still very new, churns very quickly, and is designed for tech startups and their priorities. React itself overhauls the syntax every few years, and Next.js is controlled by a young venture-capital startup whose “extinguish” phase is on the horizon. Keeping up is constant work.
So, if not that, then what? If you have a problem, but you’re rejecting what all the tech blogs are suggesting, then how can you even know a simpler alternative?
For me, I think the trick has been to eagerly learn a lot of standard tools, so I have a big flexible toolbelt for whatever comes up.
In this case, I think it’s Rails, because we’re a smallish-complexity webapp that needs just that extra oomph of help managing all its data. But for other services, we might choose something smaller like Sinatra; or in Python it might be Django or Flask; or if we need Javascript we might use something like Express… or maybe we very very carefully use Next.js with an extreme eye toward portability.
And heck, for smaller projects where you’re just throwing something together, this choice doesn’t even matter much! Which is all the more reason to go out and try things, learn new technologies, get more comfortable with smaller-scale tech like scripting and file manipulation and SQLite, so you can be ready for whatever your bigger projects throw at you.
Be sure to try stuff outside of web too—the other day I sped up the load times for a Tabletop Simulator mod by a bunch, because it was bothering me and I felt like it! A few years ago I wouldn’t have believed I could do game modding, it felt Beyond Me, but now I know! And it helps me confidently make the right decision in bigger projects, too.
Basically, to carry projects forward sustainably, I need to be acting less like a startup, and more like a hacker—because that’s what we actually are.
…Anyway, I’m sure that, after this next attempt at stabilizing the site, I’ll regret something else too—every iteration has been an artifact of the ignorance of the self I was at the time 😅
But ten years in, I know I’m getting closer, and this plan has more ring-of-truth than the one before it. I’m excited to see what I’ll learn next! And I’m grateful to know that this is one more class of mistake I won’t be making in my future work. I had to make it once, but once was enough, y’know?