The Good, Bad and the Maybe

Webtech is always changing. The times of software sitting on relatievly the same stack for ten years are gone. You’ll be lucky if the frontend you write today lasts without major user interface churn for two years, and incredibly fortunate if the ui framework doesn’t lose developer popularity or crawl to a slog in five years. The backend ecosystem is more stable, but you might say it is just as off the Rails as frontend development.

Here’s my recommendations on all things JavaScript Webtech.

Languages

We’re talking about JavaScript webtech here. If you skipped over the CoffeeScript era, that’s great. For now, the languages we really pay attention to are TypeScript and JavaScript itself.

TypeScript

If you can start a new project today with TypeScript, using TypeScript is easier. Migrating a legacy codebase is a challenge. The initial warmup time learning TypeScript feels steep, but that’s OK. You don’t need to use recursive types, you don’t need crazy interfaces. Just have it setup. Use types where they feel like inline documentation. Object shapes, arguments, and maybe that’s enough to get started.

If the tooling can support TypeScript, turn it on.

With TypeScript, you should turn on strict. It will cover so many edge cases for truthy, null and undefined checks. You will save time with this on, becuase you will avoid rewriting dozens of files.

🍌 As a cautionary plea, it’s easy to get into the groove of TypeScript feel like you’re empowered to create every typeful concoction you can think of. Resist the urge.

JavaScript

At the language level, there are occasional bursts of progress. ES6 kicked things off in 2015. Roughly eight years later, the cadence of progress is still present though sometimes it feels slower than perhaps other ecosystems (thinking about Java, surprisingly, and PHP 8.x, incidentally). Nevertheless, language churn for change’s sake is wasteful.

If you can only write plain old JavaScript, that’s fine. You will need diligence though. You will need to have additional runtime validation to ensure shapes and structures are what you say they are. You will need many more tests!

Modern Bundlers / Transpilers / Compilers

Babel and Webpack, they’re our good friends. But they’re slow now.

swc

  • swc
  • swc is written with Rust
  • SWC is sponsored by Vercel, primarily because of their integration into the “Next Compiler”
  • kdy1 is the primary core developer

esbuild

  • esbuild are the fast core tools
  • esbuild is written in Go
  • Evan Wallace is primary core developer

It’s not unlikely that another core tooling option will come around.

React and friends

It’s a React world out there. There are still other options though. Svelte, Solid, Vue. We will not mention Angular. Alpine, htmx and probably dozens more. Oh, plus jQuery.

If you’re reading this, you’re likely going to use React by default.

But, what about the others?

  • Angular
    • allegedly enterprises use Angular but luckly I do not work in such an enterprise
  • Svelte
    • I saw first hand a project repeatedly upgrade through version 1, 2 and then 3, not quite smooth, not quite a blocker; certainly less painful than AngularJS to Angular 2
  • Solid
    • I like that it keeps JSX around, being one of the strongest points of React
    • I like that it optimizes renders by not just re-rendering on every rolling and cascading state change
    • I think the hardest aspect of Solid today is, well, its ecosystem is small
  • Preact
    • Everyone loves to tell me about awesome it is that “you know this is so basic we could use Preact instead” but then you want to do one additional realisitic thing poof, the more Preact
    • Preact appears from the outside to be 80% compatible, which in userland, gets you far enough, but pulling in libraries that use more complex React flavored internals, well, you might end up out of luck
  • Vue
    • I worked with Vue 2 extensively in the 2017-2018 era, prior to React’s hooks implementation
    • At the time, I liked Vue, but I increasingly disliked Single File Components (SFCs) and found the allure of “it’s just another function in the same file” that React offered (class Components for statefulness, functional components that were pure for everything else)

There are dozens of these things. Honestly, I do not know all of them. PRs welcome.

React gets my pick today because…

  • it’s incredibly popular
  • it’s in demand
  • it has a huge practioner base
  • it has a wide, fairly thriving ecosystem
  • the middle 80% React is conceptually fine, while the outlying edges can be a bit rough

Modern React “Frameworks” for “Apps”

The React ecosystem is the largest frontend “framework” developer ecosystem for “apps”.

What is a “framework” and and what is an “app” anyway?

Single Page Application (SPA)

There’s a lot of debate about single page apps right now. With the advent of the “new” react.dev docs, and in particular the “Start a New React Project” page, there’s even more confusion and consternation about SPAs, MPAs and Hybrid apps.

There are all sorts of reasons why an SPA might have poor performance, directly impacting user expereince. Slow connections, high latencies, huge bundles, slow first paints, the list goes on.

For the most part, I think that for commodities apps like adminitistative portals, back-of-house configuration management tools, back-office management tools, data dashboards, these signle page apps are fine.

Given that, I think there are situations and circumstances where a Single Page App is not what you should reach for:

  • When you need your site and pages on Search Engine Result Pages (SERPs)
    • You’ll never get an SPA properly decorated in on the SERPs and properly ranked; not enough content, not enough meta tag control, no matter what they tell you about Googlebot crawling SPAs
    • If you’re thinking about e-commerce at all, you cannot use an SPA
  • When the majority of your pages are full of static content
    • The two other scenarios cover this; server side rendering / hybrid apps, and static site generation
  • You need robust redirection
  • When you cannot make portions of your frontend code effectively public
    • This almost never happens; put special secret sauce business logic behind APIs where they belong and your treasure will stay software

CRA

create-react-app is dead. What will happen to create-react-app?

Vite

Vite is your best bet for an SPA. It is the alternative to create-react-app.

It’s fast because it uses esbuild or optionally swc as of v4. Its community is thriving, its configuration out of the box is great, it supports TypeScript, it has a third of the dependencies of CRA. There are some downsides of course. Jest is wacky with ESM, so Vitest is a critical alternative testing tool, but the porting process is tedious although not laborious.

I have not ported an application from CRA to Vite yet, but aside from TypeScript and some other high level configuration, I can’t think of anything in the actual application code that could break if your previous CRA configuration was truly vanilla. Having gone through a few fresh Vite projects now, I am quite happy with it.

Here’s a curiosity though: the new react.dev installation guide goes deep into framework-ful frameworks. It mysteriously burries what would have been the nearest viable alternative to create-react-app in a secondary section. Now, below I also go deep into these hybrid apps but I think it is irresponsible to discard over ten years of momentum when your 2018-era feature set has just been documented.

Webpack

You might say: “hey, I have such special needs that I must have a customized webpack option.”

You’re wrong. You do not.

Server Side Rendered / Hybrid Apps

In 2021, everyone’s svelte friend Rich Harris presented the talk “Have Single-Page Apps Ruined the Web? | Transitional Apps”. In this talk, Rich discribed the pros and cons of Single Page Apps and Multi-Page Apps and tried coining a new term called Transitional Apps. I think the term fell short of popular adoption. Usually I hear this class of app as a hyrbid app. That’s fine, but I feel like we had hybrid apps for a long time, where we were doing funny tricks. This generation of hybrid apps are much more integrated and perhaps deserve that new name now.

Get your pages listed on SERPs! Get indexed today!

Rolling your own SSR with React today is not easy. Sure, you can do it, but it will be so customized, unrepeatable and unknowable. This is a Dunning Kruger decision. If you’re asking if you should build your own framework that straddles server side and client side, the answer is no. If you’re asking if you should build your own framework because you have expereince with their deficiencies, have articulated needs for their affordances and have explored many available options, then perhaps.

Next

Next and Remix are the transitional hybrid options here today.

Next has been around for a while. It has some quirks, most of them obligations of its transitional hybrid nature. Not all of these quirks are bad, but they’re things to think about, and things to be unfamiliar with if you have only worked on JSPs or CRAs (one of these is not like the other, almost by definition).

  • routing is based on the filesystem route
  • components are not colocated with their pages
  • there are weird magical functions that run if they’re defined (getStaticProps, getServerSideProps, etc)
  • i18n is a rabbit hole
  • authentication and authorization is a deeper rabbit hole
  • middleware gets a funny runtime
  • middleware gets weird behind a cdn

These quirks may change or disappear with the upcoming App Router from Next. This new mode also enables React Server Components (RSC) with Next 13 in the app/ directory.

The internals of Next are solid. It runs swc under its hood, so its fast enough. If you need some prerendered pages, some server side rendered pages (on demand) and some SPA-like functionality, then Next is great.

My hestiation is split between “can-my-team-figure-it-out” and the use case. The split between hybrid and spa are still pertinent. Next gives you great powers, but with great powers comes great responsbility, and in this case, new downsides to balance in your compromise.

  • 🙉 Do you really need your pages indexed? Not really, especially if they’re locked behind corporate firewalls or are entirely authenticated experienced.
  • 🙈 Do you have a centralized data source (instead of distributed)? With data centralization, running at the edge will not be a major latency improvement.
  • 🙊 Do you really have developers that are ready to solve entirely new types of problems that come up with state shared between the server, the edge and the browser? You might, you might not. But it’s not free. It’s bizarre to question my teams if they can use Next or not. Sure, of course they can. But they have had 6 years of full time create-react-app flavored development It’s just tough to justify.

In 2023, being a react.dev installation guide framework suggestion, surely Next is best? Well, React Server Components came around at the same time as the Next 13.4 release, and along with that, the official recommendation that Next is the framework for React. Now there’s this dichotomy between React Server Components (RSC) and Client Components, and a whole new world of integration and mindshare interia to overcome because of this flip flop. Finally, there is documented discourse and decided displeasure for having for RSC. It all still feels very beta to me (like the classical definition of beta, released, but unstable). I spent an hour one afternoon tinkering with RSCs but left it at that. It’s not ready and I am not ready.

Remix

Other than Next, there’s Remix.

Remix is still on my watch list. Hydrogen is sort of Remix too, and Remix joined Shopify. I think the tooling demos well in videos, but have not spent the deeply dedicated time to try it and tinker with it. Let’s wait another six months and see where things land.

This cannot be all there is. All of these tools are too complex for their own good. Please save us from ourselves.

Static Site Generated

Brochure-ware at its finest. Or maybe a relatively stable marketing page?

If you have markdown, or some realtively narrow set of pages with data sourced from an API, static site generation works. This can break down when you need interactive editing (for that go back to Next), or when you have so many pages rebuilding them is wasteful.

  • 🔴 Not recommended: Gatsby: it feels a lot like JavaScript flavored WordPress
  • 🟢 Tentatively Recommended: Astro: it works with everything, even React!
    • .astro files use jsx syntax, but act more like one of those new React Server Components in that they only render HTML at the end, and bring no additional JavaScript payload down
    • seriously, incredible work was done to ensure interop between React, Vue and others
    • react usage would bring in the react bundle, but its on demand and situational
    • uses vite under its hood, which in turn uses esbuild under its hood; new tools all the way down
    • successor development team to snowpack; well, hopefully it does better than that
    • island architecture is awesome; only load if it’s in the viewport and only this small chunk is impacted
    • you can even use plain js, with the bundler, and modern tooling, and packages… no framework at all
  • ✅ Cautiously Recommended: Next: it works with everything, including static sites!
    • My annual card site is built with Next and uses no SSR features
    • Next shines in this regard when coupled with its parent Vercel service or a similar edge provider
    • Static exports are available for a truly S3 class experience; but and there’s always a but, I think Next is slowly shaking this capability out of the framework having adopted this server centric approach

React flavored State Management

Redux is a lot. Sagas are too much. Redux Toolkit makes it better, but it is still a lot.

Maybe you don’t need a service bus on the frontend.

Consider:

Tree-Local-State

Components might have state, subtrees might have state, and your entire app might have state.

For subtree, or what I’ve liked calling, tree-local-state, you can do a lot with context.

  • Context often has performance issues; you put a context at the top of your entire app, you shove stuff in like Redux and your app gets slower and slower because your entire tree rerenders
  • Context is fine if you use it as a read/write 80/20 store; auth states might be fine in a Context as long as you read and you don’t write ever again
  • Really consider Context as an escape hatch for props drilling

Server & Client Shared State

react-query is usually more than enough for fetching server side state. It provides for a great developer experience, and lends itself out of the box to a pretty decent user expereince too. I can’t recommend React Query enough.

Complex State

zustand is a great option for more complex state, SPA-style cross page data propagation use cases

Like ice cream, there are plenty of state management flavors in this space. In addition to zustand, jotai and valtio are both popular too.

🍌 If you have extraordinarily complex state, Redux with Redux Toolkit has its place. It’s a slippery slope and once you go down this route and will have a hard time coming back. React accordingly.

Offline

A few years ago, I was involved with multiple projects that insisted client side state persistence for offline support. Despite all these years of development and industrial growth, I still think offline webapps are a very poor experience. Users have no idea what’s going on. Engineers barely have any better tools to make it a decent experience. In general, if offline is where your web app will be used, give all that PWA business a try and enjoy the fruitless pursuit of the perfect sw.js, but also seriously consider a real app or being dependent on generally available Internet access.

In this rare situations, I have reached for redux-offline but surely there’s something better now. Like building a real native app.

Server Side / API

With something like Rails, you can have your frontend and your backend all sort of as one thing. Even though the JavaScript ecosystem is uniquely suited towards driving the frontend and backend, the interop bridge between is still clearly in active development. Just look at Next and Remix for those examples. Even if you consider those a “backend”, they’re not the whole story. There’s no Rails. There’s no Laravel.

If anything, Next and Remix are the fabled Backends for Frontends dream fufilled, but instead of being an composition API, its composing data from API into UI. What about the actual backend, where your database lives and you interop with your own API friends?

Nest

Historically, I have promoted Nest as a viable option for an actual backend.

For Nest, I have unresolved issues, even after years of trying:

  • Annotations are half baked, not spec, and feel ergonomically odd in the language
    • They’re great when they’re magic you consume, but if you need to go any deeper, they’re just so obtuse, and it certainly does not help that TypeScript and JavaScript have no runtime reflection, so its just an illusion
  • Silly defaults
    • Why was strict off?
  • The Nest service container is kind of weird, it uses, well ironically now that I think about it, nested modules that isolate services into logical units
    • Dependency Injection (DI) isn’t for everyone, though it has its uses
    • With Spring, it all lived in one service contain freely, no modules required
    • The register/bootstrap process, unlike Laravel, isn’t clear and it’s not well described
  • TypeORM, the ORM that was highly promoted in the first party docs, was effectively abandoned
  • Integration with npm ecosystem tools is often painful
    • There are adapters for many popular libraries and patterns, but not everything
  • Dependencies… so many… dependencies
    • The more dependency surface area you have, the more time you spend auditing your security than building something useful
  • Nest still ships without a modern compiler backend (esbuild, swc) making it slower than it needs to be
  • Nest ships without new options for ESM

Arguably it is valuable in a larger team setting because it provides much needed structure. Files go here, they’re named this way, there are interfaces you can recognize and research.

I think you can use it, but you need to make the choice. The hard choice.

Express

Yep, good old Express. Express 5 will never come out of beta. It uses an API design that’s from the early days of Node and we’re stuck with it for now.

Popular middleware like Passport are beginning to age out, which is great, because the momentum that express brought will age out too and something else can come along.

Other backends

Well, why not..

  • Fastify
    • It’s definitely work looking into, but everyone knows express
  • Koa
    • Effectively a rewritten fork of Express, with async out of the box
    • Too much taken out of the box unfortunately

In the end, if you use express, with typescript, with swc, I think you have a winning combination to explore your ideas and grow your team. Then you can port that into or Go or Rust or whatever. That will be fine.

Non-JavaScript backends

I think JavaScript earns a rightful place in your backend stack, but only up to some threshold. It is not perfect by any means. If your team has more familiarity with another stack, consider that. But know that the vision of “backend for frontend” persist and it is coming sooner than later.

  • Go
    • Only recent have I become re-interested in Go, partly because I needed to write non-Node code for a project, and also for it to align with an already existing Node code suite and a team that writes primarily in Go
    • It’s very fast, it’s all very literal, it’s all very non-DRY (which is surprisingly a feature now)
    • It still feels like I am writing BASIC in Python upside down because of the lack of filter/map/reduce features languages (even Java…) have had for years
  • Java
    • Ok, I know what you are thinking, and surely I cannot be serious
    • Java 17 is actually capable of providing a modern language feature set, even richer, than dare I say, Go?
    • Spring Boot might not be everyone’s favorite but it is really quite a good target for long term software since someone will know something about it (which is the same argument used for React above)

Package Managers

  • npm has a legacy of poor performance, lagging feature sets and not being yarn
  • yarn has a legacy of being great, being fine, and having no viable upgrade path to any later version
  • pnpm is probably fine
  • berry is probably fine, but you will have to learn more about it

In many ways, npm vs yarn is like maven vs gradle, everyone picks gradle but maven hosts the registry and works as a tool OK too.

For now I recommend yarn but unfortunately everyday it marches closer to unusable’s door.

  • npm almost has feature parity now with yarn, with overrides but there’s still rough edges
  • pnpm is probably the successor we all want and need to yarn and not berry, but because it does not come pre-bundled in the baseline node docker images, I refuse to recommend it at this time
    • plus, it would yet another tool for my already “frontend” exhausted teams to know

Validation

Validation is often overlooked and undercooked.

  • 🟠 ajv
    • pro: json schema oriented
    • pro: TypeScript support
    • con: limited support for imperative validation
    • con: moving target
    • con: verbose
  • 🟠 yup
    • pro: easy to get started
    • con: mixed support for complex validations (or imperative validation)
    • con: major type-rewrite in recent history
  • ✅ zod
    • pro: easy to get started (similar to yup)
    • pro: there are adapters to convert zod schemas into json schemas

Schema validation is not the same as business logic validation. For example, if you have a schema {count: number}, that’s pretty easy to validate for all of these libraries. Suppose you need to validate the number is in a range. Also easy. What if the number has to in a fixed range, and also related to a value in a database? That feels like it is outside the scope of schema validation, but having schema validation that could do this, or share its error structure with you is worth it.

For now, I recommend zod.

XMLHttpRequest, XHR, fetch

For hybrid apps (with Next), I recommend fetch. Out of the box, Next will provide fetch in its middleware and server side. fetch will work in the browser on the frontend. fetch is great and all, though the developer experience is not always so productive. Consider building your own middleware and your own intermediate API layer to handle common situations and oddities.

For an SPA, I recommend axios. Axios has been a go to library for years. Likewise, for many years it felt like it was abandoned, languishing in a 0.x version. Now that it is 1.x, I feel better about it. I hope a 2.x variant comes around with fetch internals, now that browsers and even node have widely implemented fetch.

There are plenty of fetch shims for older browsers or older versions of node too. Like my favorite, make-fetch-happen.

got and ky might be awesome. However, part of the ecosystem is locked out of using their latest versions because of ESM incompatability.

Form management

Back in my Vue days, I used VeeValidate. We were going into a project using React Native though, so no more VeeValidate. But how could I make form validation simple and smooth just like that? In Class-based React code? I did some hacking over winter break and I came up with some kludges but nothing substantial. Good thing too. I found Formik shortly thereafter.

Forms are the worst part of any webapp.

  • 🟢 react-hook-form
    • by default, using uncontrolled inputs, react hook form manages to do all the useful form management things and keep performance much higher because the component tree isn’t always re-rendering
    • react-hook-form also has escape hatches for controlled inputs, letting you create synthetic form inputs as you need (like, for example, a non-native date picker)
  • 🟠 Formik
    • I haven’t used Formik in a while but I think it’s still widely used and most likely fine for the majority of Forms

UI libraries

You knew this was coming.

Tailwind is the only UI library you need. Then you need creativity and time.

  • 🟠 Tailwind
    • What, putting an orange caution circle on my own recommendation? Who’s better to provide constructive criticism?
    • Pros:
      • atom design at its finest
      • co-locates styles and markup structure and presentation logic together
      • makes AHA style programming easy (neither, DRY, nor WET; “prefer duplication over the wrong abstraction” or “Optimize for change first”)
    • The first major con, if you call it that, is that its still difficult to package and distribute components styled with Tailwind to other projects
    • The second minor con, if you call it that, is that paired with React and TypeScript, when you do need to make an abstraction, there’s been little guidance on how to actually do this in a safe and sane way. Perhaps with some more practice, this will get easier, and hopefully a first party adoption of tailwind-merge
    • The third patch con, if you call it that, is that Tailwind requires a mindset shift if you’re used to something else. I’ve been on the receiving end of disdain for liking and using Tailwind. It’s come up time and time again on Hacker News, always entertaining either way. Give it a try.
  • 🙅‍♂️ MUI
    • If you don’t like MUI out of the box and do not want to use it exactly it comes out of the box, maybe barring some basic color changes, then… do not use MUI
    • It is clunky beyond belief, born in a time before flex and grid became standard
    • It is clunky beyond belief, born in a time before TypeScript was effectively required in and library toolkit

There are others in this space, and they may fall somewhere between Tailwind and MUI.

Follow me on Mastodon @ryanmr@mastodon.cloud.

Follow me on Twitter @ryanmr.