Skip to content

Modernize frontend dependencies.#123

Merged
fyksen merged 33 commits into
TreshLaps:mainfrom
sesse:modernize-frontend
Jun 29, 2026
Merged

Modernize frontend dependencies.#123
fyksen merged 33 commits into
TreshLaps:mainfrom
sesse:modernize-frontend

Conversation

@sesse

@sesse sesse commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

The frontend was largely state-of-the-art in 2021, but the npm ecosystem
has moved significantly since then, and we're now struggling to even run
the stack in newer environments, much less keep up-to-date with security
fixes and the likes. This PR replaces nearly every library used with more
modern alternatives (create-react-app is replaced by Vite, react-vis
is replaced by visx, datepicker-react is replaced by native HTML5 date pickers,
CSS-in-JS is replaced by CSS modules, etc.), while still keeping to the
general spirit of a React SPA and keeping all functionality intact.
Most users should not notice a difference except for everything being a bit
faster and leaner.

CSS+JS bundle size goes down from ~791 kB to ~403 kB. (Still really big,
though!) The number of dependencies (according to npm install) goes down
from 2119 to 374. Number of known security vulnerabilities (according to
npm audit) goes down from 201 to zero. The only dependency that is not
fully up-to-date (according to ncu) after this is ESLint, where we have to
stay on version 9 because the React plugins are not yet ready for version 10.

Hopefully, this should keep us in a reasonable place for the next
couple of years, although I have no illusion that the ecosystem won't keep
changing, making this stack hopelessly outdated again in 2031.

The backend is untouched; it is still .NET 6.0.

sesse added 30 commits June 27, 2026 15:31
CRA is unloved and deprecated since 2025, so we move to Vite.
.NET is happy to run Vite as long as it outputs the right things,
and from there, it's mostly a matter of giving it the right ports
and such.

This has a bunch of knock-on effects around upgrading module,
plus some manual TypeScript fiddling. (It's possible that some
of this should already have been fixed with the dependency
upgrade earlier.) We turn off type-checking of dependencies
in tsc to reduce the amount of churn somewhat, and also ignore
some deprecations; this is a concession and should ultimately
get fixed.

This removes something like 800 (!) npm dependencies.
This also increases the minimum React version to 18. Apart from that,
routing has changed a bit (all routes are exact by default, and need to
be wrapped in a <Routes> tag), but nothing major.
This made React a bit stricter about ReactNode types, so perceivedExertion
can no longer be unknown.
The config file is auto-upgraded from the old to the new format.

We also now add an “npm run lint”.
We didn't actually use it (seemingly), and it's dead and causes
problems with some peer dependencies.
This also fixes a no-constant-binary-expression lint error.
We are now lint-clean again, except that we will get a ton of issues
if we upgrade eslint-plugin-react-hooks.
react-vis is all but unmaintained, and react-virtualized even more so.
We replace them with visx, a fairly common and a bit more lightweight
visualization library (which also uses d3 underneath). There are something
like 20 popular React charting libraries, so I basically just had to
pick something, although I first tried Rechart and didn't really get it
to click for our use. (The main problem is that the online documentation
is rather lacking.)

The goal is to get the charts broadly the same, but we do not aim for
being the same pixel for pixel; in particular, visx doesn't support
hexbin visualizations, so we use simple scatter plots instead. We mostly
use the low-level tools, which are significantly more verbose than
react-vis' XYPlot, but the equivalent XYChart seems to be pretty raw
currently (to the point where even setting the stroke of a line isn't
easily available), and visx seems to mostly be oriented around the
lower-level components. There is clearly room for improvement here.
(In the process, the Chart abstraction is no longer useful for us,
and is removed. In particular, ChartData with its string-or-number
type is problematic with stricter TypeScript typing.)

visx is touted as a very small library, but the bundle add is still
significant; that said, we go down from ~800 kB to ~475 kB bundle
after this change.

@react-spring/web is seemingly a forgotten dependency from @visx/xychart,
so we add it manually to package.json for now.
We've got a number of pacakges packages that don't go well together;
downgrade ESLint to version 9 and make some other strategic upgrades,
so that everything actually fits together. This makes npm install
work without --legacy-peer-deps.
We don't need --ignore-peer-deps or --legacy-openssl-provider anymore.
(Untested, since I don't use Docker, but at least we don't need them
when building locally.)
Fixes @typescript-eslint/no-wrapper-object-types.
This is no longer really needed, and mostly a holdover from older versions
of React, so just clean it out and replace with normal functions.
This library hasn't had a release in five years, adds almost 100 kB
bundle size, and does not easily let you move more than one month
at a time (which is exceedingly frustating when you want to look back
e.g. ten years in time). Replace it with two HTML5 date pickers instead.
…er-dom.

Newer versions of styled-components and react-router-dom have native
TypeScript support.
eslint-lugin-react-compiler has problems with mutating variables within
Array.map(), believing they mutate external state even if they stay within
the scope of the render:

  react/react#31569

Work around it by doing the cumulative sum in a for loop ahead of the map.
The linter complains badly about this; it can cause duplicate renders.
Instead, we call it before doing other set() functions.

This exposes an issue where filters are compared for object equality
instead of contents (which means React doesn't understand that it's
unchanged); it happened to go well before, but now, the separate
setLoadingState() would cause an update of the filter object and an infinite
loop. We kludge around it by doing our own explicit deep compares.

This makes us lint-clean again.
CSS-in-JS is another thing that has fallen out of style, so we
switch to CSS Modules, using Vite's native support. This means
that we get an actual .css file that the browser can parse
using its regular fast-path parser, and that is properly minified
etc.. (On the negative side, that's one more HTTP request.)
The most-hip-in-2026 solution would probably be Tailwind, but I
honestly can't bring myself to that. :-)

We map the styled components pretty much 1:1 to CSS classes
in separate files, although with so few styles, it's possible
that we should just have had a single .css file (possibly with
some tag selectors instead of class names like “.table”) and
called it a day. But this is probably the smallest semantic
change overall.
Fixes a bunch of jsx-a11y lint errors that were surfaced by the last commit
(it could not see past the styled components before).
It has not been updated in ten years or so, and is largely subsumed
by eslint-plugin-react, which we already use.
It's seemingly no longer standard to run prettier from inside ESLint;
one could use eslint-config-prettier instead, to turn off conflicting
rules, but we seem to have no such problems. So just remove it for now.
We don't use it directly; it's just a dependency of @typescript-eslint/parser
now.
Now that we don't use styled-components anymore, it's better to have
an icon set that doesn't make the dependency remain. We just use three
small SVG icons (all of them on the non-logged-in front page),
of which two are readily available in lucide-react and the third
has a semi-acceptable replacement. We save ~50 kB bundle size
and a fair amount of dependencies.
sesse added 3 commits June 29, 2026 08:36
Now that the house is mostly in order, this is just a simple upgrade.
(Unfortunately, it adds ~50 kB to our bundle size for no apparent
reason.)

The only non-current package we carry after this is ESLint 9,
because eslint-plugin-react is not yet compatible with ESLint 10.
We can do this now that everything else has been cleaned up,
and this means we can also reenable deprecation warnings.
This is also something we don't need anymore, after all the upgrades.
@sesse sesse force-pushed the modernize-frontend branch from 4f7f6ca to af9c39c Compare June 29, 2026 06:36
@fyksen fyksen requested a review from hallatore June 29, 2026 06:53
@fyksen fyksen merged commit fcbdde8 into TreshLaps:main Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants