Solid has a track record as one of the most performant, lightweight, and above all, developer-friendly front-end frameworks. Development for its second major release had started about two years ago with a lot of experiments, but without a real break-through. That changed between the years when @ryansolid overhauled the reactive system to elevate asynchronous values to first-class citizens.
Asynchronous values as first-class citizens
What does that mean? Previously, asynchronous values were handled as so-called resources in Solid. Those allowed the developer to handle them as if they were normal signals that could also be undefined or had errors. Now, you can feed any Promise or async generator function to createMemo inside a <Loading> boundary and it will return the same thing as a synchronous memo, the initial undefined state being swallowed by the boundary.
// Solid-1.x
const [data] = createResource(() => fetchUser(userId()));
// Solid-2.0
const data = createMemo(() => fetchUser(userId()));
One of the rough edges for resources were optimistic updates where you update locally while requesting the server update in the background, and then reconcile with the server values once the response arrives. There are now two new type of signals and stores called optimistic that are meant for that exact use case.
// Solid-1.x
const [message, setMessage] = createSignal<Message>(undefined);
const [messages, { mutate, refetch }] = createResource(
() => chatServer.loadMessages(),
{ initialValue: [] }
);
const [send] = createResource(
message,
(next) => chatServer.sendMessage(next)
);
const sendMessage = (next) => {
mutate([...messages(), next]);
setMessage(next);
};
createEffect(on(send, refetch));
// Solid-2.0
const [messages, setMessages] =
createOptimistic(() => chatServer.loadMessages());
const sendMessages = action(function*(next) {
setMessages(m => [...m, next]);
yield chatServer.sendMessage(next);
refresh(messages);
});
Instead of two resources being loosely coupled, we have an optimistic signal and an action. Even better, with optimistic stores, we even get the benefit of fine-grained reactivity.
Asynchronity with consequences
In order to achieve all of this, the updates that were previously synchronous became asynchronous, which requires us to deal with stale values.
// Solid-1.x
const [count, setCount] = createSignal(0);
setCount(1);
console.log(count()); // 1
// Solid-2.0
const [count, setCount] = createSignal(0);
setCount(1);
// since the updates are async,
// synchronous reading will yield stale values
console.log(count()); // 0
// however, we have two ways to get the new value:
console.log(latest(count)); // 1;
flush();
console.log(count()); // 1
Since the updates are now asynchronous, values are kept as is while the current transactions are unresolved to avoid layout jank. Using latest, we can force a synchronous resolution of a single signal or computation; with flush we can run a synchronous resolution on all currently batched actions.
The second part of handling stale values is that we need to split effects into subscription and computation, since the latter might need to be delayed until the next resolution of the reactive state, but we already need to know what signals the effect is subscribed to:
// Solid-1.x
createEffect(() => apiCallA(signalB()));
// Solid-2.0: similar to createEffect(on(...))
createEffect(signalB, apiCallA);
// multiple subscriptions use tuples:
createEffect(
() => [signalA(), signalB()],
([a, b]) => a && apiCall(b)),
);
Lastly, we want to know if certain values are still being resolved; we can use isPending(() => [memo1, signal2]), which allows fine-grained control over which changes to monitor for unresolved state.
The name game
Other than this, there were a few names that changed:
-
solid-js/webnow has its own library@solidjs/weband also exportsisServer -
<Suspense>➔<Loading>(only holds rendering until first async load) -
<SuspenseList>➔<Reveal> -
onMount➔onSettled(a return function is now cleanup) -
<Index>has been merged into<For keyed={false}> - Store Setters now work like auto-reconcile, path setting is still supported via
storePathhelper -
unwrap(store)➔snapshot(store) -
mergeProps/splitProps➔merge/omit
House cleaning
The previously supported /*@once*/ pragma and directives have been removed to unify special cases; instead, use untrack and ref (which now supports an array of ref functions). classList, which had some inconsistencies when used together with class, has also be removed; in exchange, class now both supports objects and arrays:
// Solid-1.x
<button
class="button"
classList={{
[`variant-${props.variant}`]: true,
disabled: props.disabled
}}
>
{props.children}
</button>
// Solid-2.0
<button
class={[
"button",
`variant-${props.variant}`,
{ disabled: props.disabled },
]}
>
{props.children}
</button>
Putting changes in Context
The return of createContext is now the context provider itself:
// Solid-1.x
const Theme = createContext("light");
<Theme.Provider value="dark">...</Theme.Provider>
// Solid-2.0
const Theme = createContext("light");
<Theme value="dark">...</Theme>
In addition, useContext will now throw if it doesn't find a context in its Parent scopes, so it will always return the context type, not T | undefined.
// Solid-1.x
const theme: string | undefined = useContext(Theme);
if (theme === undefined)
throw new Error('Theme Provider is missing!');
// theme is now string
// Solid-2.0
const theme: string = useContext(Theme);
// will throw automatically if provider is missing
What's more in store?
Stores now get mutable proxies in their setters; the previous path selection can still be used with the storePath helper:
// Solid-1.x
setState('todos', id, 'done', true);
// Solid-2.0
setState(x => { x.todos[id].done = true; });
// or
setState(storePath('todos', id, 'done', true));
Also, reconcile now allows keyed detection of items, e.g. for a list that contains objects that have a stable id field, you can use
// Solid-1.x
setState(reconcile(nextTodos);
// Solid-2.0, faster position detection:
setState(reconcile(nextTodos, "id"));
// previous behavior:
setState(reconcile(nextTodos, () => true));
This brings some performance improvements if items in an array change position.
Also, stores can now be derived in both read-only and writable ways; what createMemo is for signals, createProjection is for stores, createStore(fn, seed) allows to do the same as writable projection.
const active = createProjection((s) => {
const id = activeId();
s[id] = true;
if (s._prev) delete s[s._prev];
s._prev = id;
}, {});
// use as `active[id]` to only trigger changed elements
Agentic awesomeness
One thing that the migration has uncovered is that coding LLMs still had a hard time writing Solid-1.x code, especially since they are mostly trained on React code, which is superficially similar, yet with very different underlying principles. Solid-2.0 now ships with specialized documentation aimed at solving these issues so that your agentic helpers will support it better.
And what about everything else?
While the current version is still in beta, it is very usable and there are first projects in the wild already using it. For @solidjs/testing-library, you can use "solidjs/solid-testing-library#next" in your package.json to support the new changes. A pre-release of our community primitives is currently being prepared and many third-party libraries are also catching up right now. Early versions of storybook-solid are already working.
TanStack Start already supports Solid-2.0; A rough integration into SolidStart 2.0 exists, with the remaining issues being worked on.
As always, the community on the Solid Discord is active and helpful, should you have any questions.
Top comments (0)