We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

SolidJS, signals and a component that runs once

SolidJS looks like React and works differently, a component runs once and a signal updates. Version 2.0 in beta, SolidStart, and habits that mislead.

SolidJS, signals and a component that runs once

SolidJS is a library for building interfaces that uses the same syntax as React and works on an entirely different principle. The current stable version is 1.9, under the MIT licence, with version two in beta since March 2026.

The syntactic resemblance is its greatest marketing advantage and its greatest trap while learning. The code looks familiar, so somebody arriving from React writes it from day one, carrying over habits that make no sense in this model.

A component runs once

That is the sentence to understand before anything else starts making sense.

In React a component function executes on every state change, and the library compares the result with the previous one and updates the differences. Here a component function executes once, at creation, and builds reactive links between signals and specific places in the document.

Code
TypeScript
import { createSignal, createEffect } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)

  console.log('this prints once')

  createEffect(() => {
    console.log('this prints on every change', count())
  })

  return <button onClick={() => setCount(count() + 1)}>{count()}</button>
}

The print outside the effect appears once, regardless of how many clicks follow. Changing the signal updates only the part of the document that reads it, with no re-execution of the function.

The consequences run deep. There is no dependency list, since links form automatically at the moment of reading. There is no need to memoise functions or values, since nothing gets recreated. There are no rules about where a hook may be called, since there is no call order to preserve.

React habits that mislead

Worth listing them concretely, since they cause most trouble on a first project.

Reading a signal is a function call rather than reading a variable. Passing the value instead of the call breaks the link and the fragment stops updating, with no error at all.

Code
TypeScript
const [count, setCount] = createSignal(0)

<Label value={count()} />

<Label value={count} />

The first form reads the value once, at component creation, and stays at zero forever. The second passes the function itself, so the recipient reads it at its own site and the link survives. That is the most common slip and the hardest to spot, since everything looks correct and nothing reports an error.

Destructuring props into separate variables also breaks reactivity. A value read once at component creation stays that way forever, and props must be read through the object at the moment of use.

Code
TypeScript
function Card({ title, description }) {
  return <h2>{title}</h2>
}

function Card(props) {
  return <h2>{props.title}</h2>
}

If you do need to split props anyway, to pass some of them onward for instance, the library provides a dedicated helper that preserves reactivity.

Code
TypeScript
import { splitProps } from 'solid-js'

function Button(props) {
  const [own, rest] = splitProps(props, ['variant'])
  return <button class={own.variant} {...rest} />
}

Conditional rendering through an early return does not behave as expected. Since the function executes once, the condition gets checked once. Conditional rendering uses built in components taking the condition as a property.

The same applies to lists. Rendering an array through ordinary mapping works and forfeits this model's advantage, since every change recreates all elements.

Code
TypeScript
import { Show, For, Index } from 'solid-js'

<Show when={user()} fallback={<SignIn />}>
  {(u) => <Profile user={u()} />}
</Show>

<For each={tasks()}>
  {(task) => <Row task={task} />}
</For>

<Index each={numbers()}>
  {(number, i) => <Field value={number()} index={i} />}
</Index>

The difference between the last two is worth remembering, since picking the wrong one costs performance. The first compares items by reference and suits lists where entries are added, removed, and reordered. The second compares by position and suits fixed length lists where the values change, form fields for instance.

Practical advice: on a first project, read the section on reactivity pitfalls before writing a first component. Those four things account for most situations where a newcomer concludes something is broken.

Version two and first class async

Version two has been in beta since March 2026, and its headline is what had been missing: the reactive graph understanding promises.

Until now, asynchronously fetched data required separate mechanisms and loading state got handled by hand. In the new version a promise is a value the reactive graph understands, so waiting for data stops being something beside reactivity and becomes part of it.

A reworked mechanism for suspending rendering during loading joins that, along with predictable batching of changes, meaning the things that in practice decide how much flicker a user sees.

The most important thing for somebody deciding today: this is a beta. Material describing the new approach to async concerns something you cannot put into production without deliberately accepting risk. The stable line remains 1.9.

Note too that the framework built on this library, SolidStart, went its own way. Its second version shipped as stable on 4 August 2026 and replaced the previous intermediate layer with plain Vite. Importantly it builds on the library's stable 1.9 line rather than on the version two beta, so neither release waits on the other.

Performance and what follows

In comparisons this library consistently sits near the top, close to solutions with no framework. Worth knowing when that advantage is felt and when it is a number in a table, though.

It is felt with frequent, small updates. A table of a thousand rows where one cell changes every second behaves differently here from tree comparison. So does a view refreshed by data arriving on a stream.

It is also felt in bundle size. No tree comparison layer means less code shipped to the browser, which on pages judged by load time matters measurably.

It is not felt in a typical form application. A dashboard with lists and forms, where changes follow a user's click, runs equally fast in every popular option, since the bottleneck is the server request rather than rendering.

The practical conclusion: performance is a good argument when your application genuinely does something that strains it. As the main argument for an ordinary dashboard it settles a dispute that does not exist.

Stores and nested state

A signal holds a single value, and that suffices for a number, a string, or a flag. For objects and arrays the right tool is a store.

The difference is granularity. A signal holding an object reacts to the whole object being replaced, so changing one field notifies everybody reading anything from that object. A store tracks fields separately, so changing a user's name does not refresh the fragment displaying their address.

Writing to a store looks like ordinary assignment, though a tracking mechanism runs underneath. That is convenient and carries one pitfall: a nested structure requires naming a path rather than replacing the whole thing, since replacement loses exactly the granularity the store was chosen for.

The practical rule: signals for simple values, stores for structures. A signal holding an array of a hundred objects is a design smell, since every change to one element invalidates everything reading that array.

Note too that stores are built in and require no separate state management library. Application wide state gets created as a store outside components and imported where needed, with no provider wrapping the tree. That simplifies a project more than the description suggests, since a whole category of decisions disappears: where to put the provider and how to split the contexts.

Server rendering and SolidStart

The library itself supports server side rendering, and the framework around it adds routing, data loading, and building.

The data loading model resembles the one familiar from route based frameworks: data gets fetched at the route level, before rendering, so there is no cascade of requests firing as components mount.

Server functions let you call server side code from a component without writing a separate interface route. That is convenient and carries the same pitfall as everywhere: the function looks local while being a network call, so validation and permissions belong inside it rather than in the component calling it.

A project status worth noting when planning: the framework's second version is already stable, released on 4 August 2026, and builds on plain Vite instead of the previous intermediate layer. Older material still describes it as alpha or beta, so check the date on any guide you land on. The framework itself runs on the library's 1.9 line, so its stability does not depend on the version two beta.

Check the availability of supporting libraries before deciding too. Routing, forms, and data handling have solutions here, while for more specific needs the list of options is short and writing something yourself enters the calculation more often than in larger ecosystems.

SolidJS against the alternatives

OptionModelEcosystemPick it when
SolidJSSignals, component runs onceSmallFrequent small updates, a small bundle
ReactRe-renderingThe largestThe job market, library availability
AngularSignals, a framework with everythingLargeA large team, imposed structure
VueReactivity, a middle ground frameworkLargeA gentle entry, a smaller team

Something worth saying plainly: on reactivity model the gap against the others narrowed. Signals reached Angular and Vue, and the topic keeps returning in React, so the argument about this approach's uniqueness is weaker today than two years ago.

Closest to this model sits Qwik, which also rests state on signals, only it subordinates them to resuming an application rather than to update speed. Its second version stays in beta and the ecosystem there is narrower still, so it counts as an alternative for content sites rather than for a live data dashboard.

What remains is consistency. Here signals are not an addition beside an existing model but the whole model, so there are no two ways of doing the same thing and no compatibility layer with an older approach.

The deciding factor on a commercial project is usually different, though, and deserves naming honestly. The ecosystem is small. Component libraries, integrations, and ready solutions exist in numbers incomparable with the first row, and few people know this tool. On a team meant to maintain it for years, that factor outweighs the performance difference.

When reaching for it makes sense

Three cases where the choice holds up under cold calculation too.

The first is a widget embedded on other people's pages. A small bundle and no dependency on a heavy runtime translate directly into load time on a page where you are a guest.

The second is an interface with many small updates: a live data dashboard, an editor, a visualisation reacting to an event stream. There the reactivity model translates into smoothness you can see.

The third is a personal project or a team deliberately wanting to work in this model. That justification is as good as any, provided the decision is made in full awareness of the ecosystem's size and of who will be maintaining this in two years.

Outside those three, justifying the choice on a commercial project with a long horizon is hard, and saying so plainly beats pretending performance wins everything.

Common mistakes

The first is passing a signal's value rather than its call. The link breaks, the fragment stops updating, and no error appears.

The second is destructuring props into separate variables. A value read once at component creation stays that way forever.

The third is conditional rendering through an early return. The function executes once, so the condition gets checked once.

The fourth is rendering lists through ordinary mapping. It works and recreates every element on every change, forfeiting this model's main advantage.

The fifth is expecting the hooks familiar from React. There are no dependency lists and no memoisation here, since there is nothing to memoise, and hunting for equivalents leads the wrong way.

The sixth is building on version two for a production project. It is a beta, and the stable line remains 1.9.

The seventh is holding structures in a signal rather than a store. Changing one field invalidates everything reading that object, so the fine grained reactivity advantage disappears exactly where it was meant to work.

The eighth is validating a server call on the component side. The function looks local while being a network call that can be made bypassing the interface, so data and permission checks belong inside it.

FAQ

How does SolidJS differ from React?

In its update model. Here a component function executes once and builds links between signals and places in the document, so changing a signal updates only those places. In React the function executes on every change and the library compares the result with the previous one.

Is version 2.0 ready?

No, it has been in beta since March 2026, and the stable line remains 1.9. Material describing first class async concerns that beta, so check the current state before a production deployment.

Why did my fragment stop updating?

Most often because a signal's value was read somewhere instead of passing its call, or props were destructured into separate variables. Both break the reactive link and report no error.

Is it worth choosing for a commercial project?

That depends on horizon and team. The model is coherent and the performance real, while the ecosystem is small and few people know this tool. On a project maintained for years that factor usually outweighs the performance difference.

What is SolidStart?

A framework built on this library, adding routing, server rendering, and building. Its second version shipped as stable on 4 August 2026 and builds on plain Vite, while running on the library's 1.9 line, so it does not drag the version two beta along with it.

Documentation sits on the project site, and releases in the GitHub repository.