SOLIDJS: THE ROAST
Let me paint you a picture.
It's 2018. Ryan Carniato looks at React and says "I can do better." He's right. He creates SolidJS, a framework so fast it makes React look like it's running on a 1997 Gateway. The benchmarks don't lie. The DX is clean. The reactivity is fine-grained.
And then he just... keeps writing code. Alone. For nearly eight years.
git shortlog -sn --all | head -5:
1592 Ryan Carniato
33 Damian Tarnawski
30 Xavier Loh
23 Dan Jutan
19 Joe Pea
1,592 commits. The second place contributor has 33. That's not a team. That's one man playing multiplayer with himself. Every design decision, every bug fix, every "wip the legend continues" commit message - that's one brain making every call for an entire framework ecosystem.
Ryan, buddy. You built something beautiful. But you also built a bus factor of exactly 1, and that bus is running React in production.
BIT #1: THE TYPE SYSTEM GAVE UP
packages/solid/src/reactive/signal.ts:1772
type TODO = any;
That's it. That's the type. In a file with 1,808 lines of the most intricate reactive primitives this side of a physics simulation, someone looked at a type problem and said "you know what? TODO."
git blame shows Joe Pea committed this in November 2021. That's FOUR years ago. Four years of "TODO = any" sitting at the bottom of the core signal file.
But wait. It's not just sitting there. It's being USED:
packages/solid/src/reactive/signal.ts:836
fn: EqualityCheckerFunction<T, U> = equalFn as TODO,
packages/solid/src/reactive/signal.ts:977
for (let i = 0; i < deps.length; i++) (input as unknown as TODO[])[i] = deps[i]();
"as unknown as TODO" - that's DOUBLE casting. First to unknown (the "I give up" type), then to TODO (which is... also giving up). It's giving up twice. It's the type system equivalent of "no, seriously, I really don't care."
This framework has 204 @ts-expect-error and @ts-ignore comments.
The signals.type-tests.ts file has this gem at lines 985 and 1019:
// FIXME
Just... FIXME. No description. No context. Just a comment crying into the void.
BIT #2: "TERRIBLE DE-OPT" - SELF-AWARE CODE
packages/solid/src/reactive/signal.ts:1789
// terrible de-opt
Owner.context = { ...Owner.context, [ERROR]: [fn] };
mutateContext(Owner, ERROR, [fn]);
Ryan Carniato wrote this on August 9, 2023. Over two years ago. He LABELED it "terrible de-opt." And then he shipped it anyway. The comment isn't a TODO. It's not a FIXME. It's an acceptance. A confession.
He didn't say "temporary workaround" or "needs optimization." He said "terrible" - the code knows what it is, and it's at peace with that.
There's also one in server/reactive.ts:369:
// terrible de-opt
Same thing. Copy-pasted shame across two files.
This is the SolidJS philosophy in action: acknowledge the sin, ship it anyway, benchmark faster than React, and let the terrible de-opts become someone else's problem in the heat death of the universe.
At least he's honest. Most frameworks bury their terrible de-opts under seven layers of abstraction and call them "optimization opportunities."
BIT #3: "INSPIRED BY" - THE POLITE WORD FOR IT
packages/solid/src/reactive/signal.ts:1
// Inspired by S.js by Adam Haile, https://github.com/adamhaile/S
packages/solid/src/reactive/array.ts:17
// Modified version of mapSample from S-array by Adam Haile
Every single reactive primitive in SolidJS - the createSignal, createMemo, createEffect that people rave about - is "inspired by" S.js. The MIT license at the top of signal.ts isn't even Solid's license. It's Adam Haile's, from 2017.
Ryan didn't invent fine-grained reactivity. He REFINED someone else's invention, wrapped it in better DX, and then spent seven years getting credit for it on Twitter.
S.js has 1.4k stars. SolidJS has 34k. Adam Haile is still maintaining S.js without a fraction of the hype. The commit message for S.js's last update doesn't have "wip the legend continues" in it.
Don't get me wrong - refinement matters. Solid IS better than S.js in many ways. But every time someone says "Solid's reactivity is revolutionary," Adam Haile's name should echo somewhere in the distance.
This is software development. We all stand on shoulders. Ryan's shoulders just have a louder microphone.
BIT #4: "THE WIP CHRONICLES"
Let's look at what Ryan's been up to in 2025:
2025-01-22 16:04:41 -0800 wip the legend continues
2025-01-16 16:43:58 -0800 more wip
2025-01-09 17:23:02 -0800 wip
2025-05-07 17:38:37 -0700 sync hydration wip, faking depth wip
2025-05-13 15:23:18 -0700 hydration wip
2025-05-16 08:31:19 -0700 more ssr/hydration wip
2025-04-30 12:31:58 -0700 more SSR WIP
2025-04-11 08:15:06 -0700 more ssr wip
"wip the legend continues" - this man is writing his own commit message fanfic.
Seven WIP commits just in 2025. SSR/hydration alone has been "WIP" for most of the year. This is Solid 2.0 we're talking about - the rewrite that's been in experimental for... *checks* ...11 experimental versions now.
2025-02-13 12:37:50 -0800 v2.0.0-experimental.0
2025-12-05 16:16:29 -0800 v2.0.0-experimental.11
Ten months. Eleven experimental releases. Still WIP.
The tests have this gem:
packages/solid/web/test/suspense.spec.tsx:137
// Await the rest of the things, TODO: figure out what these are
They're ASSERTING things pass without knowing WHAT those things are. The test works. The developer doesn't know why. This is production JavaScript.
BIT #5: "THE LONELY VAR IN A CONST WORLD"
packages/solid/src/reactive/signal.ts:51
export var Owner: Owner | null = null;
"var."
In 2025. In TypeScript. In the core reactive system of a framework that prides itself on modern JavaScript.
This isn't some legacy code. This is signal.ts - THE file. The 1,808-line heart of everything SolidJS does. And right there on line 51, sitting between a sea of const and let declarations:
const signalOptions = { equals: equalFn };
let ERROR: symbol | null = null;
let runEffects = runQueue;
...
export var Owner: Owner | null = null;
Just one. A single "var" surviving like a cockroach through nearly eight years of development. Nobody changed it. Nobody questioned it. It just... exists.
I can hear the justification now: "It needs to be hoisted for the reactive graph to work correctly across module boundaries." Sure. Maybe. Or maybe someone wrote var in 2018 and nobody's touched that line since because it works and we don't touch things that work.
The var is load-bearing. Do not remove the var.
BIT #6: "THE ART OF NOT CARING ABOUT TYPES"
Count with me. In packages/solid/src/:
- as unknown as - 15 occurrences
- as any - 21 occurrences
- @ts-expect-error - 204 across the codebase
- @ts-ignore - scattered throughout
Here are some greatest hits:
packages/solid/src/reactive/signal.ts:977
(input as unknown as TODO[])[i] = deps[i]();
This is "as unknown as TODO" where TODO is already defined as "any". They're casting to unknown, then to any, then treating it as an array. Three layers of "the type system can't help you now."
packages/solid/src/render/flow.ts:170
const ch = chs() as unknown as MatchProps<unknown> | MatchProps<unknown>[];
packages/solid/src/render/flow.ts:209
? (conditionValue() as any)
packages/solid/src/render/Suspense.ts:69
const res: SuspenseListState = [] as any;
That last one is declaring an array and immediately lying about what it is. "This is a SuspenseListState," they say, creating an empty array. The type system trusts them. The type system shouldn't.
packages/solid/src/render/component.ts:275
return target as any;
Just... returning the thing and giving up. After 275 lines of carefully orchestrated property merging logic, the final answer is "as any."
BIT #7: "DEPRECATED SINCE... WHEN EXACTLY?"
packages/solid/src/reactive/signal.ts:1775
* @deprecated since version 1.7.0 and will be removed in next major
* - use catchError instead
Version 1.7.0 came out... let me check... the current stable is 1.9.10. That's three minor versions. "Next major" has been experimental.0 through experimental.11 for ten months.
The deprecated function? Still there. Still exported. Still functioning. Probably still used in production by people who never read changelogs.
Meanwhile:
packages/solid/src/reactive/signal.ts:67
/** @deprecated use `afterRegisterGraph` */
afterCreateSignal: ((signal: SignalState<any>) => void) | null;
No version mentioned. No timeline. Just "deprecated" floating in the void. When will it be removed? Maybe never. Maybe tomorrow. The chaos is the point.
packages/solid/src/render/component.ts:70
/** @deprecated: use `ParentProps` instead */
At least this one tells you what to use instead. Progress.
SolidJS deprecation policy: announce it once, never remove it, let it haunt the type definitions forever like a ghost that pays rent.
BIT #8: "THE FIXME GRAVEYARD"
packages/solid/test/signals.type-tests.ts has 1,129 lines. Let me show you what lives in those lines:
Line 30: // @ts-expect-error FIXME prev is inferred as unknown
Line 45: // @ts-expect-error FIXME prev is inferred as unknown
Line 163: // @ts-expect-error FIXME prev is inferred as unknown
Line 178: // @ts-expect-error FIXME prev is inferred as unknown
Line 296: // @ts-expect-error FIXME prev is inferred as unknown
Line 311: // @ts-expect-error FIXME prev is inferred as unknown
Line 468: // @ts-expect-error FIXME prev is inferred as unknown
Line 486: // @ts-expect-error FIXME prev is inferred as unknown
Line 662: // @ts-expect-error FIXME computed type is unknown
Line 674: // @ts-expect-error FIXME computed type is unknown
Line 688: // @ts-expect-error FIXME computed type is unknown
Line 703: // @ts-expect-error FIXME computed type is unknown
Line 722: // @ts-expect-error FIXME computed type is unknown
That's 13+ instances of the SAME TYPE BUG, copy-pasted through a test file with @ts-expect-error so it doesn't fail the build.
And then there's:
Line 985: // FIXME
Line 1019: // FIXME
Just... FIXME. A cry for help. A monument to technical debt. These comments have been there long enough to watch TypeScript release three major versions.
The test file is simultaneously proving the types work AND documenting all the ways they don't. It's Schrodinger's type safety.
BIT #9: "THE FEATURE THAT WASN'T"
packages/solid/src/server/rendering.ts:643
// TODO: support revealOrder and tail options
This is in the SuspenseList function. The ENTIRE function is:
export function SuspenseList(props: {
children: string;
revealOrder: "forwards" | "backwards" | "together";
tail?: "collapsed" | "hidden";
}) {
// TODO: support revealOrder and tail options
if (sharedConfig.context && !sharedConfig.context.noHydrate) {
const c = sharedConfig.context;
setHydrateContext(nextHydrateContext());
const result = props.children;
setHydrateContext(c);
return result;
}
return props.children;
}
You see those props? revealOrder and tail? The entire point of the component? They accept them. They TYPE them. They do NOTHING with them.
It returns props.children. That's it. The "forwards", "backwards", "together" reveal orders? Meaningless. The "collapsed" and "hidden" tail options? Theater.
The types say SuspenseList can coordinate reveal order. The implementation says it returns children and goes home early. This is a function accepting configuration it ignores.
Somewhere, someone is debugging why their SuspenseList revealOrder="backwards" isn't working. The answer: it never worked. The TODO is the feature.
THE VERDICT
Look. SolidJS is genuinely, objectively, measurably GOOD.
It's faster than React. The DX is cleaner. The reactivity model makes sense. Ryan Carniato is legitimately brilliant and has done more for frontend performance education than most framework authors combined.
But here's the thing about brilliant solo developers: they write brilliant code that only they understand.
- 1,808 lines in signal.ts
- 1 maintainer who matters
- 204 TypeScript escape hatches
- Nearly 8 years of "wip the legend continues"
- Functions that accept options they ignore
- Tests that pass for reasons unknown to the test author
- A type called TODO that's just any wearing a costume
This isn't a framework. This is one man's brain externalized into TypeScript. It works because Ryan knows how it works. When Ryan doesn't know, there's a FIXME. When TypeScript doesn't know, there's as unknown as TODO.
Solid is incredible technology built on the shoulders of S.js, documented with prayer and TODO comments, maintained by someone who has been shipping WIP commits to experimental branches for nearly a decade.
Use it. It's fast. Just know that when you createSignal, you're invoking code that's one refactor away from someone asking "wait, why is this a var?"