Releases: solidjs/solid
v1.9.0 - LGTM!
This release like the last is focusing on small quality of life improvements and adjustments that will help us move towards 2.0. So while not the most exciting release to everyone it provides some really important features and fixes to some developers.
And unlike many previous releases the vast majority of the work and features came from PRs from the community. So really all I can say is Looks Good to Me!
Better JSX Validation
While still incomplete across templates we've added JSDOM to the compiler to better detect invalid HTML at build time by comparing what we expect the template to be with what a browser would output. This now includes things that are nested we didn't detect before like putting <a>
inside other <a>
tags which will lead to the browser "correcting" it in less than intuitive ways.
Improved Exports
While each environment in solid-js/web
has its own methods to be used in the compiler. We are now exporting the client methods from the server to prevent weird import errors. Now these methods will throw if used in this environment but shouldn't break your build.
Additionally we have seen some issues in bundlers that incorrectly feed our ESM exports back through the browser field. While this is a known issue they all pointed issues at each other and with no intention of fixing it. We have removed the browser field in this release, meaning some legacy packages may have issues resolving browser if they don't support export conditions.
This is regretful but this blocked deployments on several platforms and since this was the only fix at our disposal after two years of attempting to push this issue to the bundlers to no avail, we've moved forward with it.
Custom Element improvements
We have a few improvements to our custom element support in this release. First off we now detect elements with the is
attribute as custom elements which means all the special behavior is afforded to them.
We've also improved our event handler delegating retargetting to better handle shadow DOM events. There were cases where we skipped over part of the tree.
Finally we've added the bool:
attribute namespace to handle explicitly setting certain attributes according to boolean attribute rules. While this isn't necessary for built-in booleans currently we handle most attributes as properties and we lacked a specific override. But now we have it:
<my-element bool:enable={isEnabled()}></my-element>
Support for handleEvent Syntax in Non-Delegated Events
A little known thing is that events actually also support objects instead of functions (See: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
We(thanks @titoBouzout) realized we can use this mechanism as a way to set advanced rules like passive
or capture
on this object as way to handle all current and future event attributes that browsers might add. This way we don't need specific mechanisms like oncapture:
(which is now deprecated).
Instead using on:
you can set the event properties you wish.
<>
<div on:click={{
handleEvent(e) {
console.log("clicked", e)
},
once:true
}/>
<div on:wheel={{
handleEvent(e) {
e.preventDefault() // only works on not passive events
e.stopPropagation()
console.log("wheel stopped?")
},
passive: false
}} />
</>
Other Updates
We've fixed an issue with lazy images. Apparently, cloneNode doesn't handle them properly so we've updated our heuristic to treat templates with lazy images to be handled with importNode
.
We've improved our Hydration Mismatch Error to output the template of that it can't find the matching ID for. This should make it easier to track down where the hydration errors are occurring. There have been several hydration improvements over the later 1.8 releases so upgrading will likely improve the situation for those who have been facing issues.
Finally, we've improved some of the types in the JSX and Signal Setter in this release.
Big thanks to those who contributed to this release: @wkelly17, @olivercoad, @titoBouzout, @trusktr, @Huliiiiii. And thanks to all of you who gave feedback on the Metadata/Head Tag RFC. While it didn't make it in this time around you've definitely given us stuff to consider for its future design.
Best,
@ryansolid
v1.8.0 - Bifröst
In Norse mythology, Bifröst (/ˈbɪvrɒst/), is a burning rainbow bridge that reaches between Midgard (Earth) and Asgard, the realm of the gods.
I am first to admit this is not the most exciting release from a feature standpoint. I was looking for cool code snippets but these changes are for the most part all under the hood. We are in that time period between the end of 1.x and the start of 2.0. We recently made our new reactive experiments public and continue to build those out in public with @solidjs/signals. We need to build for the future while bridging the gap with the present.
This version is about addressing some of the fundamentals that will help projects like SolidStart move forward while we do the transition. Async and Resources need work and are too all in. It is great to have a solution but now that we have a better understanding we need to start breaking things apart into their fundamental pieces and exposing them in ways we can better leverage. And in Solid 1.8 a big part of that is rethinking serialization, our bridge between the server and the client.
A lot of our learning here has come from applying what we have learned when doing performance benchmarks for the work that has been funded by Google Chrome Aurora. In order to realize our goal we've had to update the foundations.
De-duping Streaming Serialization
This is the marquee feature of this release and is largely the work of @lxsmnsyc. Solid has been able to serialize promises and do streaming for a couple of years now, but it was very special-cased. Now it is a generic mechanism.
This matters because it means that we have decoupled the promise serialization from Resources, and in so decoupled the whole "when the stream is done" from them. This opens up things like nested promises and being able to serialize more complicated data structures.
We now also have a mechanism that deeply de-dupes data serialized across flushes. If the data is ever serialized during the request we won't send it again even if sent in different streaming chunks. This is important for features like Islands where you might pass the same props to multiple Islands across different Suspense boundaries and don't want to send the data more than once. This is very beneficial where that data can be accessed at varying depths (recursive comments in say a Hackernews site) or other recursive data structures.
Hydration Improvements
Fragments for Hydration have been a bit of a pain and we keep seeming to have different issues reported around element duplication. Most commonly this has been around where there are lazy
component siblings or where the fragment is top-level. After looking into and fixing an issue for Astro I decided to look at some of the oldest bugs in Solid and found it was a similar bug.
In many cases, the DOM can change throughout Hydration while doing things like streaming but we need to pause and resume hydration because code isn't available yet. While we don't create elements during hydration, getting an accurate snapshot of the DOM for the current state for future list reconciliation is a process we've had a few tries at but in 1.8 we update this in a way that makes sure it doesn't get out of date.
Also in 1.8, we have added some performance improvements to hydration in the form of not redundantly setting attributes or props as the page hydrates similar to how we don't update text. This is all migration towards a future where we don't need to do as much hydration, but it is important to note that values will be kept as they were on the server rather than how they may compute at runtime during hydration. In so we will be erroring more aggressively when mismatches happen.
Smaller Templates
In 1.7 we removed unnecessary closing tags from template strings. It was a bit painful because we were a bit overzealous at first. While I believe in the end we got to a good place, ultimately all but the simplest reductions have been hidden behind a compiler flag(omitNestedClosingTags
). Thanks to work from @intrnl we are implementing another template size reduction technique of removing unnecessary quotes. Quotes are actually not required by HTML in some cases and it can add up.
Other
Fix NGINX Server Side Includes
Comments led with #
are treated as special directives for a few different servers so we've needed to change our open hydration markers to $
. As usual, your version of Solid and the Babel Plugin should be the same to ensure this matches up.
Better Guards on Global Scripts
Solid uses an inline HydrationScript as a way to do processing before the framework and code have loaded. To handle things like event capture and streaming. However, we didn't do a good job of guarding the right thing when multiple were added to the same page, a situation that can happen in Micro-frontends or 3rd party Islands solutions. Now the script guards against duplicate inclusion.
With another release out, I'd like to thank everyone involved in this release and in the various efforts across our ecosystem. From everything from the Docs effort to the Playground infrastructure. We've started to tighten up on features for 1.x and moving towards our 2.0 efforts, so we ask for a bit of patience but in the coming months as we put out some RFCs for 2.0 I look forward to engaging you in discussions on the future of Solid.
Thanks,
@ryansolid
v1.7.0 - U Can't Type This
Solid has experienced incredible growth in usage the past 6 months. Companies are using it to power production applications and SolidStart Beta has been a big part of that. As a natural part of this growth and increased use at scale we are continuing to learn what works well and what the rough edges in Solid are today.
This v1.7 release marks the beginning of the migration roadmap to v2.0. We are beginning to re-evaluate core APIs and will begin introducing new ones while reasonably deprecating older ones in a manner that eases breaking changes. Our intention is to ease the broader ecosystem into preparing for improvements that a major 2.0 will unlock for the whole community.
Improved TypeScript
Null-Asserted Control Flow
One of the pains of using Solid with TypeScript has been that JSX control flows can't really type narrow. This is true, but starting with the migration to explicit keyed
in v1.5 we now complete this story by introducing callback forms for <Show>
and <Match>
that work when non-keyed.
The main difference is the callback form instead of passing in the value as it does when keyed
, passes in a function that is type narrowed.
// keyed w/ callback - reruns full callback on change
<Show when={user()} keyed>
{nonNullUser => <div>{nonNullUser.name}</div>}
</Show>
// non-keyed w/o callback... - only updates the one expression, needs ! assertion
<Show when={user()}>
<div>{user()!.name}</div>
</Show>
// NEW!
// non-keyed w/ callback - only updates the one expression
<Show when={user()}>
{nonNullUser => <div>{nonNullUser().name}</div>}
</Show>
Keep in mind because we are non-null asserting the input signal so it won't expect null in closures that execute when the condition is no longer satisfied. For this reason the accessor from the callback is special and will throw when attempted to be accessed when the condition is no longer true. This may be unexpected but it is our best attempt to keep TypeScript strict and not present inconsistency in reactivity. Luckily this only applies to things like timers which you should be cleaning up anyway and not things like event handlers. We recommend using the original conditions source in those closures if you must.
Better Event Types for Input Elements
This has irked people for a while but we come by it honestly, target
is gives you a type of Element
rather than the specific element that is the target. That means no access to .value
or .checked
. The reason is there is no way to know at compile time what the target of an event will be. The currentTarget
will be the element you attach the event to but the target can be anything.
There is a way to work around this though, in that if we know the currentTarget
is of type that generates the event and that the currentTarget
is the the type of this element we can assume it is the target
as well. Not perfect logic but it is what React does and we do too.
Now onInput
, onChange
, onBlur
, onFocus
, onFocusIn
, and onFocusOut
all support more detailed target
when applied to HTMLInputElement
, HTMLTextAreaElement
, and HTMLSelectElement
.
Stricter JSX Elements
Strict JSX elements have been tricky because we have to acknowledge at a certain point that TypeScript is to serve our purposes rather than to represent all possible values that could work. For us the ambiguity lies in functions.
Solid's JSX needs to accept functions to handle dynamic insertion. However, in authoring it leads to awkward situations.
You first hit this the first time use Solid. You create that counter and don't call count
as a function and it works.
function Counter() {
const [count, setCount] = createSignal(1);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
This example works in some places and not others which might lead to the wrong conclusions.
The second place you might hit this is when you get a little further on your journey and decide you need a component to re-render and decide that you can just wrap the whole thing in a function:
function MyComp(props) {
return () => {
// look working early returns
if (props.count > 5) {
return <div>Maximum Tries</div>;
}
return <div>Attempt {props.count}</div>;
};
}
Again this seems fine, except the fact that every time count
changes you are recreating all the DOM Elements even when it resolves to the same conditional.
Eventually you might even not think twice about passing functions into children of arbitrary components:
<MyComp>
<MyComp2>
<MyComp3>{() => <div>{resource()}</div>}</MyComp3>
</MyComp2>
</MyComp>
But what does this do? When is the function called?
As it turns out removing functions from JSX.Element
type makes all of these scenarios error. Components only expect the values dictated by their types.
function MyLayout(props: { children: JSX.Element }): JSX.Element;
function MyFor<T, U extends JSX.Element>(props: { each: T[], children: (item: T) => U }): JSX.Element;
// valid
<MyLayout>Hello</MyLayout>
<MyLayout><p>Hello</p></MyLayout>
<MyLayout>{name()}</MyLayout>
<MyLayout>{name() && <p>Hello</p>}</MyLayout>
<MyLayout>{(() => {
return <p{name()}</p>
})()}</MyLayout>
<MyLayout>{untrack(() => {
return <p>{name()}</p>
})}</MyLayout>
<MyFor each={users()}>{(user) => <div>{user.name}</div>}</MyFor>
// invalid
<MyLayout>{name}</MyLayout>
<MyLayout>{() => <p>Hello</p>}</MyLayout>
<MyLayout>{() => "Hello"}</MyLayout>
<MyLayout>{() => name() && <p>Hello</p>}</MyLayout>
<MyFor each={users}>{(user) => <div>{user.name}</div>}</MyFor>
<MyFor each={users()}><div>Not a Function</div></MyFor>
The tradeoff here is that authoring components you can no longer just return a Signal or Memo without casting. If using JSX you can always return a Fragment.
If not you will need to cast to unknown as JSX.Element
.
Better Errors
catchError
replaces onError
Error Handling is complicated enough without having to try to guess how they propagate. onError
admittedly is a lower level primitive but fundamentally had this flaw. It worked by registering an error handler on the parent scope, but left it ambiguous how to handle siblings. Is it a queue? Are they independent?
As a result we are introducing catchError
in this release which introduces its own scope to catch any errors below it. The first argument in the primitive is similar to the try and the second argument is the catch.
catchError(
() => {
// do stuff
throw new Error("I've Errored");
},
err => console.log(err)
);
onError
will still be present until it can be removed in a future major version.
Standardized Errors
Error Handling has had many weird edge cases introduced by applications throwing unusual values. In v1.7 we wrap all thrown values that aren't of type Error
in a new Error
and attach the original thrown value as .cause
.
More Performant Dev Tools
Now that Solid Dev Tools have been stabilizing, we have a much better idea what support we need for them. In so we were able to remove the very costly serialization we were doing for generating unique identifiers. Conventions around naming and exports were streamlined and standardized as well.
Others
- Smaller compiled output, remove auxilary closing tags
- Support for
prop:
andattr:
in Spreads - Don't apply special props (like
readonly
) to custom elements - Reverse cleanup execution order at the same level
- Introduced improved serializer, seroval
- Fixed quirks in Solid's treeshaking in Rollup
- Minify inline class and style attributes
- Update
solid-ssr
to type"module"
- Test Suite moved to Vitest
Big thanks to @lxsmnsyc, @thetarnav, @niccholaspage, @atk, @Exelord, @jpdutoit, @titoBouzout, @MarkChrisLevy, @ulivz for your contributions and to everyone who contributed missing JSX types.
Also to @LiQuidProQuo, @fabiospampinato for challenging decisions. This was the most difficult release we had in terms of making decisions due to the impact it has on TypeScript so it was valuable seeing the other opinion.
Finally special shoutout to @otonashixav, who is our guiding light in terms of TypeScript and is the only way a release like this happens.
v1.6.0 - Castle in the Sky
For the past months we've been working away at SolidStart and that has led to some incredible exploration into the realm of islands, partial hydration, and hybrid routing. Solid 1.6 backfills that into the core so that other Solid projects can benefit.
We've also addressed a long-time issue with the proper merging of JSX spreads on native elements. When components don't re-execute dynamic spreads can one of the most complicated pieces to preserve granularity. Overall this is a smaller release but it represents a unique innovation into our vision of the future.
Special thanks to @nksaraf and @rturnq work to guide exploration in these directions and contributions from @trusktr, @LiQuidProQuo, and @thetarnav
Highlights
Official Partial Hydration Support
Solid has worked for quite some time in partial hydrated ("Islands") frameworks like Astro, Iles, Solitude, etc.. but now we have added core features to support this effort better. These features are mostly designed for meta-framework authors rather than the end user they are exposed through a couple APIs.
<Hydration />
joins <NoHydration />
as being a way to resume hydration and hydration ids during server rendering. Now we can stop and start hydratable sections. This is important because it opens up a new optimization.
createResource
calls under non-hydrating sections do not serialize. That means that resources that are server only stay on the server. The intention is that hydrating Islands can then serialize their props
coming in. Essentially only shipping the JSON for data actually used on the client. Reducing the double data problem significantly.
The power here is static markup can interleave dynamic components.
<h1>Server Rendered Header</h1>
<Island>
<h2>Server Rendered Sub Header</h2>
<p>{serverOnlyResource().text}</p>
<DifferentIsland>
<p>More server-renderd content</p>
</DifferentIsland>
</Island>
Keep in mind Server rendered content like this can only be rendered on the server so maintaining client navigation with this paradigm requires a special router that handles HTML partials.
Similarly, we want the trees to talk to each other so hydrate
calls now have been expanded to accept a parent Owner
this will allow Islands to communicate through Contex without shipping the whole tree to the browser.
<h1>Server Only Rendered Header</h1>
<ClientProvider>
<h2>Server Only Rendered Sub Header</h2>
<ClientIslandThatReadsContext />
</ClientProvider>
These improvements make it easier to create Partial Hydration solutions on top of Solid, and serve to improve the capabilities of the ones we already have.
Native Spread Improvements
Native spreads are something we started at very naively. Simply just iterating an object that has some reactive properties and updating the DOM element. However, this didn't take into consideration two problems.
First properties on objects can change, they can be added or removed, and more so the object itself can be swapped. Since Solid doesn't re-render it needs to keep a fixed reference to the merged properties. Secondly, these are merged. Properties override others. What this means is we need to consider the element holistically to know that the right things are applied.
For Components, this was never a problem since they are just function calls. Unfortunately for native elements, this means all those compiler optimizations we do for specific bindings now need to get pulled into this. Which is why we avoided it in the past. But the behavior was too unpredictable.
In 1.6 we have smartened spread to merge properly using a similar approach to how we process Components.
// A`class` property in `props` now takes priority even if someSignal() updates.
// In fact it won't even be subscribed to unless props does not have a `class` property.
<div class={someSignal()} {...props} />
We've also found new ways to optimize the experience. (See below).
Other Improvements
Deproxification
Working on new Spread behavior we realized that while we can't tell from compilation which spreads can change. We can tell at runtime which are proxies. And in so if we only need to merge things that don't swap, and aren't proxies we can avoid making a Proxy.
What is great about this is it has a cascading effect. If component props aren't a proxy, then splitProps
and mergeProps
don't need to create them, and so on. While this requires a little extra code it is a real win.
We get a lot requests for low-end IoT devices because of Solid's incredible performance. In tests, Solid outperforms many of the Virtual DOM solutions in this space. However, most of them don't support proxies.
So now if you don't use a Store
or swap out the props object:
// this is fine
<div {...props} />
// these could swap out the object so they make proxies
<div {...props.something} />
// or
<div {...someSignal()} />
We don't need to introduce any proxy the user didn't create. This makes Solid a viable option for these low-end devices.
Note on the title
Castle in the Sky was a landmark film for director Hayao Miyazaki, the first created at their newly formed studio, Studio Ghibli back in 1985. I found the imagery of floating Islands irresistible in the sense with our client-routed Islands it is like pulling them out of the water.
A "Castle in the Sky" also refers to an idealistic, almost unrealizable goal. I think our isomorphic ambition of where we are taking things in frontend development feels like that sometimes. Attempting to keep aloft balancing the restrictions of the weight of JavaScript, the desire for interactivity, and the polish of experience seamlessly across both platforms immediately brings to mind these floating structures. A Solarpunk-esque return to our roots in server rendering with advanced technology that attempts to make itself invisible.
So whether that is for you, Laputa, Skyloft, Zeal, Skypiea, Sanctaphrax, or some other, I wish you luck on your journey upwards.
Best, @ryansolid
v1.5.0 - Batch to the Future
The last couple of months has been about looking toward the future. With Solid 1.0 clearly in the rearview, we are preparing for the possibility of Solid 2.0. We've started the working group to discuss new ideas and a lot of thought has been going into that future. At the same time, SolidStart, our new full-stack starter is getting closer to its initial reveal we've learned a lot about server rendering that has made it back into the core.
We're very happy today to release the next version of Solid, thanks primarily to the contributions of @modderme123, @edemaine, @trusktr, @otonashixav, @lxsmnsyc, @LiQuidProQuo, @Drevoed, @nksaraf, @paoloricciuti, and @jorroll.
Key Highlights
New Batching Behavior
Solid 1.4 patched a long-time hole in Solid's behavior. Until that point Stores did not obey batching. However, it shone a light on something that should maybe have been obvious before. Batching behavior that stays in the past is broken for mutable data. Solid only has createMutable
and produce
but with these sorts of primitives, the sole purpose is that you perform a sequence of actions, and batching this properly defies expectation. You wouldn't expect adding an element to an array and then removing another item to just skip the first operation, but that is proper behavior when values stay in the past.
const store = createMutable(["a", "b", "c"]);
const move = store.splice(1, 1);
store.splice(0, 0, ...move);
// solid 1.4
// ["b", "a", "b", "c"];
// solid 1.5
// ["b", "a", "c"];
After a bunch of careful thought and auditing, we decided that Solid's batch
function should behave the same as how reactivity propagates in the system once a signal is set. As in we just add observers to a queue to run, but if we read from a derived value that is stale it will evaluate eagerly. In so signals will update immediately in a batch now and any derived value will execute on read. The only purpose of batch
now is to group writes that begin outside of the reactive system, like in event handlers or async callbacks.
More Powerful Resources
Resources continue to get improvements. A common pattern in Islands frameworks like Astro is to fetch the data from the outside and pass it in. In this case, you wouldn't want Solid to do the resource fetching or the serialization on the initial server render, but you still may want to pass it to a resource so it updates on any change. For that to work reactivity needs to run in the browser. The whole thing has been awkward to wire up but no longer.
ssrLoadFrom
field lets you specify where the value comes from during ssr. The default is server
which fetches on the server and serializes it for client hydration. But initial
will use the initialValue
instead and not do any fetching or additional serialization.
const [user] = createResource(fetchUser, {
initialValue: globalThis.DATA.user,
ssrLoadFrom: "initial"
});
We've improved TypeScript by adding a new state
field which covers a more detailed view of the Resource state beyond loading
and error
. You can now check whether a Resource is "unresolved"
, "pending"
, "ready"
, "refreshing"
, or "error"
.
state | value resolved | loading | has error |
---|---|---|---|
unresolved | No | No | No |
pending | No | Yes | No |
ready | Yes | No | No |
refreshing | Yes | Yes | No |
errored | No | No | Yes |
A widely requested feature has been allowing Resources to be stores. While higher-level APIs are still being determined we now have a way to plugin the internal storage by passing something with the signature of a signal to the new Experimental storage
option.
function createDeepSignal<T>(value: T): Signal<T> {
const [store, setStore] = createStore({
value
});
return [
() => store.value,
(v: T) => {
const unwrapped = unwrap(store.value);
typeof v === "function" && (v = v(unwrapped));
setStore("value", reconcile(v));
return store.value;
}
] as Signal<T>;
}
const [resource] = createResource(fetcher, {
storage: createDeepSignal
});
Consolidated SSR
This release marks the end of a several year long effort to merge async and streaming mechanisms. Since pre 1.0 these were separate. Solid's original SSR efforts used reactivity on the server with different compilations. It was easiest to migrate synchronous and streaming rendering and for a time async had a different compilation. We got them on the same compilation 2 years ago but the runtimes were different. Piece by piece things have progressed until finally async rendering is now the same code path as streaming if flushing was deferred until everything has completed.
This means some things have improved across the board. Async triggered Error Boundaries previously were only ever client rendered (throwing an error across the network), but now if they happen any time before sending to the browser they are server-rendered. onCleanup
now runs on the server if a branch changes. Keep in mind this is for render effects and not true side effects as not all rendering cleans up.
Finally, we've had a chance to do a bunch of SSR rendering performance improvements. Improved raw string rendering by about 8% and replaced our data serializer with an early copy of @DylanPiercey from Marko's upcoming serializer for Marko 6 which boasts performance improvements of up to 6x over devalue
which we used previously. Also, we streamlined asset and script insertion.
Keyed Control Flow
Solid's <Show>
and <Match>
control flows were originally re-rendered based on value change rather than truthy-ness changing. This allowed the children to be "keyed" to the value but led to over rendering in common cases. Pre 1.0 it was decided to make these only re-render when statement changed from true
to false
or vice versa, except for the callback form that was still keyed.
This worked pretty well except it was not obvious that a callback was keyed. So in 1.5 we are making this behavior explicit. If you want keyed you should specify it via attribute:
// re-render whenever user changes
// normal
<Show when={user()} keyed>
<div>{user().name}</div>
</Show>
// callback
<Show when={user()} keyed>
{user => <div>{user.name}</div>}
</Show>
However, to not be breaking if a callback is present we will assume it's keyed. We still recommend you start adding these attributes (and TS will fail without them).
In the future, we will introduce a non-keyed callback form as well so users can benefit from type narrowing in that case as well.
Other Improvements
children.toArray
Children helper now has the ability to be coerced to an array:
const resolved = children(() => props.children);
resolved.toArray(); // definitely an array
Better SSR Spreads
Finally fixed spread merging with non-spread properties during SSR, including the ability to merge children.
Better Error Handling
We weren't handling falsey errors previously. Now when Solid receives an error that isn't an Error
object or a string it will coerce it into an Unknown Error
.
Migrated the Repo to pnpm and TurboRepo
Thanks @modderme123 for taking the time to completely modernize our build setup. Making it way easier to contribute and drastically improve build and testing performance.
NodeNext Support
TypeScript 4.7 brought a new way to use package exports to resolve Types and with Solid 1.5 we now support this.
Improved Tagged Template Literals
Many fixes have been made to Tagged Template literal parsing (special thanks @trusktr) fixing many rough edges.
Improved CSS Types
Solid now uses csstype
for better typing of the style property.
in
support in Stores
The in
operator is now tracked and auto-wrapped in the JSX.
Bug fixes
Many small bug fixes, including:
- fragment hydration mismatch
- head/body elements included in templates
- comparison functions not called with no observers
- scheduling error in concurrent rendering
- deletion on createMutable
As always thanks to everyone involved who contributed, not only to the repos but the countless discussions across Github and Discord. Not everything ended up getting in this round and we had some difficult decisions like reviewing writing symbols to stores, and consistency of boolean attributes. Even if we landed on no change these discussions they served to re-enforce the good patterns we have in Solid today and gave a lot of insight into what we can do in the future.
Sincerely,
@ryansolid
I apologize for the terrible pun
v1.4.0 - Level Up!
This release has been a long time coming. 1.3 brought a ton of new features and a ton of new bugs that have been ironed out in the last couple of months. SolidHack also ran over this time where we got see people pushing the limits of what can be done. We've taken the feedback and applied it to our messaging, documentation, and our roadmap.
v1.4 addresses some of these allowing people to do more with the features we already have. It may not have as many new toys to play with, but it makes significant improvements to how we use Stores, Resources, and Streaming.
There also has been an incredible amount of work on TypeScript. So much so that I can't even include all the improvements in this post. Huge thanks goes out to @otonashixav, @trusktr, @edemaine, as well as review by @atk, @high1, and others to help see these landed.
New Features
Top Level Arrays in Stores
Since Stores were first introduced it has always bugged me that the most common case, creating a list, required nesting it under a property to track properly. Thanks to some exploration into proxy traps and iteration we now support top-level arrays. In addition to its other modes, the Store setter will accept an array that allows for common operations.
const [todos, setTodos] = createStore([
{ id: 1, title: "Thing I have to do", done: false },
{ id: 2, title: "Learn a New Framework", done: false }
]);
// set at an index
setTodos(1, done, true);
// use an array
setTodos([...todos, { id: 3, title: "New Todo", done: false }])
// iterate over it with <For>
<For each={todos}>{todo => <Todo todo={todo} />}</For>;
Through this change, we also stopped over execution when listening to specific properties. To support iteration Solid previously would notify the owning object of an array when any index was added/removed or object when a new property was created or a property was deleted.
The one caveat is downstream optimized control flow that untrack index reads on arrays will now need to track the iterated object explicity. Solid exports a $TRACK
symbol used to subscribe to the object and all its properties.
Resource Deferred Streaming
Streaming brings a lot of performance benefits but it also comes with the tradeoff we need to respond with the headers before we can send any content. This means we must set the Response headers early if we want to benefit from streaming. While it's always possible to fetch first and delay rendering that slows down everything. Even our async server rendering doesn't block rendering but instead just waits to respond to the end.
But what if you want to stream but also want to wait on some key data loading so you still have an opportunity to handle the response on the server before sending it to the browser?
We now have the ability to tell Solid's stream renderer to wait for a resource before flushing the stream. That you can opt-in by setting deferStream
option.
// fetches a user and streams content as soon as possible
const [user] = createResource(() => params.id, fetchUser);
// fetches a user but only streams content after this resource has loaded
const [user] = createResource(() => params.id, fetchUser, { deferStream: true });
When this happens the stream and the response will be held allowing the server to process what it needs to before sending the status and streaming back the response. This is also valuable if there is some high-priority content that is worth waiting for before sending the response.
Stale Resource Reads
Suspense and Transitions are amazingly powerful features but occasionally you want to opt-out of the consistency and show things out of date because it will show up faster and some of the things you are waiting for are not as high priority. In so you want the Transition to end sooner, but not necessarily stop showing the stale data for part of the screen. It is still preferable to receding back to loading spinner state.
Solid's Resources now support being able to read the value without triggering Suspense. As long as it has loaded previously latest
property won't cause fallback to appear or Transitions to hold. This will always return the latest
value regardless of whether it is stale (ie.. a new value is being fetched) and will reactively update. This is super powerful in Transitions as you can use the Resource's own loading
state to know if it is stale. Since the Transition will hold while the critical data is loading, the loading state will not be applied to the in view screen until that Transition has ended. If the resource is still loading now you can show that it is stale.
const [resource] = createResource(source, fetcher)
// read it as usual
resource();
// read the latest (don't suspend if loaded at least once)
resource.latest;
Example: https://codesandbox.io/s/solid-stale-resource-y3fy4l
Combining multiple Custom Renderers
The Babel plugin now allows configuring multiple custom renderers at the same time. The primary case it is so a developer can still lever Solid's optimized DOM compilation while using their custom renderer. To make this work specify the tags each renderer is reponsible for. It will try to resolve them in order.
import { HTMLElements, SVGElements } from "solid-js/web";
let solidConfig = {
moduleName: "solid-js/web",
// @ts-ignore
generate: "dynamic",
renderers: [
{
name: "dom",
moduleName: "solid-js/web",
elements: [...HTMLElements, ...SVGElements]
},
{
name: "universal",
moduleName: "solid-three",
elements: []
}
]
};
Credit for this goes to @nksaraf who has been working on weaving Solid's optimal DOM compiler with a custom Three.js compiler.
Improvements/Fixes
Synchronous Top Level createEffect
These were originally deferred to a microtask to resemble how effects are queued under a listener. However it is more correct to run immediately like everything else top level.
Better Types around Components
This one took the effort of many resident TypeScript experts, but we've now landed on some better types for components. The biggest change is Component
no longer has an opinion on whether it should have children
or not. We've added supplementary types ParentComponent
and FlowComponent
to denote Components that may have children
or always have children
. And we've added VoidComponent
for those which may never have children.
Sources in createResource
are now Memos
A small change but it was unusual to have refetching trigger a reactive expression outside of a reactive context. Now on refetch it grabs the last source value rather than re-running it.
Better handling of null
and undefined
Spreads and classes in the JSX now handle these values without writing them as strings or throwing exceptions.
createMutable
batches array methods like push, pop, etc..
Now these built-ins are batched and more performant. For instance using push
or splice
will only trigger updates once.
We've also added modifyMutable
that applies modifiers batched to stores created with createMutable
.
modifyMutable(state.data.user, reconcile({ firstName: "Jake", middleName: "R" }));
Better Support for React JSX transform
We have added support to solid-js/h
to support the new React JSX transform. You can use it directly in TypeScript by using:
{
"jsx": "react-jsx",
"jsxImportSource": "solid-js/h"
}
Keep in mind this has all the consequences of not using the custom transform. It means larger library code, slower performance, and worse ergonomics. If you use this remember to wrap your reactive expressions in functions.
HyperScript now returns functions
This one is a potentially breaking change, but the current behavior was broken in a different way. It was possible(and common) for children to be created before the parents the way JSX worked. This was an oversight on my original design that needs to be fixed, as it breaks context, and disposal logic. So now when you get your results back from h
you need to call it. Solid's render
function will handle this automatically.
const getDiv = h("div", "Hello");
document.body.appendChild(getDiv()); // call as a function to have it create the element.
Special thanks to @fabiospampinato for identifying and bringing this long-standing issue to our attention.
Removals and Deprecations
className
, htmlFor
deprecated
While they still work for now, Solid will remove support for these React-isms in a future version. They leave us with multiple ways to set the same attribute. This is problematic for trying to merge them. Solid updates independently so it is too easy for these things to trample on each other. Also when optimizing for compilation since with things like Spreads you can't know if the property is present, Solid has to err on the side of caution. This means more code and less performance.
Experimental refetchResources
removed
This primitive ended up being too general to be useful. There are enough cases we can't rely on the refetch everything by default mentality. For that reason, we are dropping support for this experimental feature.
v1.3.0 - Spice Must Flow
This release is a big one. It really focuses on improving Server Side Rendering. We add support for Streaming HTML, Isomorphic Error Boundaries, and Multiple Async Hydration Roots. We also made some small tweaks to resources to make it easier than ever to build powerful query caching solutions on top of them. And we have started experimenting with supporting even more types of reactivity.
There are a lot of new features in this release many experimental. So feedback is very appreciated. There also are a few small API changes in this release to facilitate future features, so be sure to read the breaking changes below.
New Features
HTML Streaming
This release adds support for HTML streaming. Now we not only stream data after the initial shell but the HTML as it finishes. The big benefit is that now for cached results or times when the network is slow, we no longer have to show the placeholder while waiting for the JavaScript bundle to load. As soon as the HTML is available it will be streamed and inserted.
With it comes a new streaming API renderToStream
. This is a universal API designed to handle both Node and Web writable streams. It returns an object that mirrors a Readable stream on both platforms that has both pipe
(node) and pipeTo
(web). The benefit of this pipe
API is the user can choose when to insert the content in the output stream whether soon as possible, or onCompleteShell
, or onCompleteAll
. This decouples Solid's rendering from the stream a bit but leaves things open to performance improvements in the future.
// node
const stream = renderToStream(() => <App />).pipe(res);
// web
const stream = renderToStream(() => <App />).pipeTo(writable);
Error Boundaries on the Server
We've added support for Error Boundaries on the Server for all rendering methods(renderToString
, renderToStringAsync
, renderToStream
). Errors can be caught both from synchronous rendering and from errors that happen in Resource resolution. However, Our approach doesn't guarantee all errors are handled on the server as with streaming it is possible that the Error Boundary has already made it to the browser while a nested Suspense component hasn't settled. If an Error is hit it will propagate up to the topmost Suspense Boundary that hasn't been flushed yet. If it is not handled by an Error Boundary before that it will abort rendering, and send the Error to the browser to propagate up to the nearest Error Boundary.
This works now but there is more to explore here in improving Error handling in general with SSR. So look forward to feedback on the feature.
Isolated Server Render/Hydration Contexts
Sometimes you want to server render and hydrate multiple Solid apps on the same page. Maybe you are using the Islands architecture with something like Astro. We now have the ability to pass a unique renderId
on all our server rendering methods and to the hydrate
function. This will isolate all hydration and resource resolution. This means we can use things like server side Suspense in these solutions.
Also now you only need to include the Hydration Script once on the page. Each Island will be responsible for initializing it's own resources.
// on the server
const html = renderToString(() => <Island1 />, { renderId: "island1" });
// for the browser
hydrate(() => <Island1 />, mountEl, { renderId: "island1" });
createReaction
This new primitive is mostly for more advanced use cases and is very helpful for interopt with purely pull based systems (like integrating with React's render cycle). It registers an untracked side effect and returns a tracking function. The tracking function is used to track code block, and the side effect is not fired until the first time any of the dependencies in the tracking code is updated. track
must be called to track again.
const [s, set] = createSignal("start");
const track = createReaction(() => console.log("something"));
// next time s changes run the reaction
track(() => s());
set("end"); // "something"
set("final"); // no-op as reaction only runs on first update, need to call track again.
This primitive is niche for certain use cases but where it is useful it is indispensible (like the next feature which uses a similar API).
External Sources (experimental)
Ever wanted to use a third party reactive library directly in Solid, like MobX, Vue Reactivity, or Kairo. We are experimenting with adding native support so reactive atoms from these libraries can be used directly in Solid's primitives and JSX without a wrapper. This feature is still experimental since supporting Transitions and Concurrent Rendering will take some more effort. But we have added enableExternalSource
enable this feature. Thanks @3Shain for designing this solution.
import { Reaction, makeAutoObservable } from "mobx";
import { enableExternalSource } from "solid-js";
import { render } from "solid-js/web";
let id = 0;
enableExternalSource((fn, trigger) => {
const reaction = new Reaction(`externalSource@${++id}`, trigger);
return {
track: x => {
let next;
reaction.track(() => (next = fn(x)));
return next;
},
dispose: () => {
reaction.dispose();
}
};
});
class Timer {
secondsPassed = 0;
constructor() {
makeAutoObservable(this);
}
increase() {
this.secondsPassed += 1;
}
reset() {
this.secondsPassed = 0;
}
}
// component driven directly off MobX
function App() {
const timer = new Timer();
setInterval(() => {
timer.increase();
}, 1000);
return <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>;
}
render(() => <App />, document.getElementById("app"));
refetchResources
(experimental)
In efforts to allow for scaling from simple resources up to cached solutions we are adding some experimental features to createResource
to work with library writers to develop the best patterns. Caching is always a tricky problem and with SSR and streaming being part of the equation the core framework needs at minimum to provide some hooks into orchestrating them.
Sometimes it's valuable to trigger refetch
across many resources. Now you can.
import { createResource, refetchResources } from "solid-js";
const userCache = {};
function MyComponent(props) {
const [data] = createResource(
() => props.id,
(userId, { refetching }) => {
const cached = userCache[userId];
// return cached value if available and not refetching
if (cached && !refetching) return cached;
return fetchUser(userId);
}
);
}
// somewhere else
refetchResources();
You can also pass a parameter to refetchResources
to provide additional information to the refetching
info of the fetcher. This could be used for conditional cache invalidation. Like only refetch resources related to users
. This mechanism requires a bit of wiring but the idea is you'd wrap createResource
in maybe a createQuery
and implement your own conventions around resource cache management. Still working out how this should work best, but the goal is to provide the mechanisms to support resource caches without being responsible for their implementation.
To opt-out being part of the global refetch createResource now takes a globalRefetch
option that can be set to false. In addition to a new option to disable refetchResources
there is no an onHydrated
callback that takes the same arguments as the fetcher. When a resource is restored from the server the fetcher is not called. However, this callback will be. This is useful for populating caches.
Improvements
Better TypeScript Support
Thanks to the tireless efforts of several contributors we now have significantly better types in Solid. This was a huge effort and involved pulling in maintainers of TypeScript to help us work through it. Thank you @trusktr for spearheading the effort.
Better SourceMaps
Work has been done to improve sourcemaps by updating babel-plugin-dom-expressions
to better preserve identifiers from the JSX. Thanks to @lxsmnsyc for exploring and implementing this.
Breaking Changes/Deprecations
startTransition
no longer takes callback as a second argument
Instead it returns a promise you can await. This works better for chaining sequences of actions.
const [start, isPending] = useTransition();
start(() => doSomething()).then(() => allDone());
Resource fetcher info object replaces getPrev
To streamline API for refetch we are slightly updating the createResource
:
const [data] = createResource(sourceSignal, (source, { value, refetching }) => {});
For those using existing 2nd argument:
const [data] = createResource(sourceSignal, (source, getPrev) => {
const value = getPrev();
});
// becomes
const [data] = createResource(sourceSignal, (source, { value }) => {});
Deprecating Legacy Streaming APIs
pipeToNodeWritable
and pipeToWritable
are deprecated. They will still work for now with basic usage but some of the more advanced options didn't map over to the new APIs directly and have been removed. Move to using renderToStream
.
Bug Fixes
- Fixed browser extensions modifying the head element breaking hydration.
- Fixed reinserting
<html>
on hydration from document. - Fixed over-executing on multi-select with
createSelector
. - Fixed event delegation conflicting with document event listeners.
- Fixed self owning source infinite recursion.
- Fixed faulty treesplitting for hydration in client only render.
- Fixed return type of
preload
on lazy components to always be a promise. - Fixed compile error with leading white space after opening tags when generating ssr.
v1.2.0 - Masters of the Universe
This release centers around the addition of the universal JSX transform and runtime generator to enable custom renderers. This opens up whole new worlds of possibility with the opportunity to support new platforms beyond the web.
New Features
Custom Renderers
This release adds support custom renderers through a new "universal" transform. Solid now provides a sub module solid-js/universal
that exports a createRenderer
method that allows you to create your own runtimes. This will enable things like native mobile and desktop, canvas and webgl, or even rendering to the terminal. This is still new so very much looking for feedback.
// example custom dom renderer
import { createRenderer } from "solid-js/universal";
const PROPERTIES = new Set(["className", "textContent"]);
export const {
render,
effect,
memo,
createComponent,
createElement,
createTextNode,
insertNode,
insert,
spread,
setProp,
mergeProps
} = createRenderer({
createElement(string) {
return document.createElement(string);
},
createTextNode(value) {
return document.createTextNode(value);
},
replaceText(textNode, value) {
textNode.data = value;
},
setProperty(node, name, value) {
if (name === "style") Object.assign(node.style, value);
else if (name.startsWith("on")) node[name.toLowerCase()] = value;
else if (PROPERTIES.has(name)) node[name] = value;
else node.setAttribute(name, value);
},
insertNode(parent, node, anchor) {
parent.insertBefore(node, anchor);
},
isTextNode(node) {
return node.type === 3;
},
removeNode(parent, node) {
parent.removeChild(node);
},
getParentNode(node) {
return node.parentNode;
},
getFirstChild(node) {
return node.firstChild;
},
getNextSibling(node) {
return node.nextSibling;
}
});
Working example here.
Spreads Added to Solid's html
It's been a long time coming but Solid's Tagged Template Literals now support element and component spreads using htm inspired syntax.
html`<div ...${props} />`
Fixes
Dynamic Spreads now work on Components
Previously spreads on components would only track property changes on bound objects and not when the whole object changed. This now works:
<MyComponent {...getStuff()} />
ClassList properly merges multiple classnames in the key
It is common in libraries like Tailwind to apply multiple classes at the same time. There was an issue where true and false resolutions were cancelling each other out. This would only set text-sm
.
<div
classList={{
"px-2.5 py-1.5 text-xs": false,
"px-3 py-2 text-sm": false,
"px-4 py-2 text-sm": true,
"px-4 py-2 text-base": false,
"px-6 py-3 text-base": false
}}
/>
Consistent handling of HTMLEntities
Things like
used to render differently depending if in elements or components(or fragments). This has been made consistent across all three.
Various improvements to Types and Transitions
A lot of bugs from the last minor release were around Transitions that have been addressed. And as always Types have been gradually improving.
V1.1.0 - Interrupting Cow
Expanding Solid's concurrency to include interruptible scheduling. Bug fixes around Types and around reactive execution order guarantees.
New Features
createUniqueId
A universal id generator that works across server/browser.
const id = createUniqueId();
Note on the server this only works under hydratable components
from
A simple helper to make it easier to interopt with external producers like RxJS observables or with Svelte Stores. This basically turns any subscribable (object with a subscribe
method) into a Signal and manages subscription and disposal.
const signal = from(obsv$);
It can also take a custom producer function where the function is passed a setter function returns a unsubscribe function:
const clock = from(set => {
const t = setInterval(() => set(1), 1000);
return () => clearInterval(t);
});
Note: Signals created by
from
have equality checks turned off to interface better with external streams and sources.
enableScheduling
(experimental)
By default, Solid's concurrent rendering/Transitions doesn't schedule work differently and just runs synchronously. Its purpose is to smooth out IO situations like Navigation. However, now you can opt into interruptible scheduling similar to React's behavior by calling this once at your program's entry. I've yet to see a realworld scenario where this makes a big difference but now we can do cool demos too and start testing it.
startTransition
Works like its counterpart in useTransition
, this useful when you don't need pending state.
import { createSignal, startTransition } from "solid-js";
function App() {
const [signal, setSignal] = createSignal("Howdy");
function clickHandler(e) {
startTransition(() => setSignal("Holla"));
}
/* ...stuff */
}
v1.0.0 - Solid grows up
Well, the time has come. We're happy to announce Solid 1.0. This is the culmination of 5+ years of work, but in another sense this is just the start. We have marked the APIs as stable(except for a couple that have (experimental)
next to them, like SuspenseList). What started as a quest to show that reactivity still had it has become a fully-featured JavaScript UI Framework.
While the 1.0 release doesn't bring behavior differences it does bring some breaking syntax changes. We've worked to make the simple Signals and our proxy objects have a more consistent API. And we've move the proxies out into their own sub module, renaming them from "State" to "Store". We feel this will reduce confusion and better reflect their purpose.
There is only a single 1.0.0 release, the first major version, and I am so happy and thankful to be here. Everyone who has been instrumental in us getting here has my eternal gratitude. From those who were always ready with encouraging words, to the great group of contributors and core maintainers we've built up today.
So thank you, and enjoy.
Breaking Changes
setSignal now supports function form
While that in itself is a great new feature as you can do:
const [count, setCount] = createSignal(0);
setCount(c => c + 1);
This promotes immutable patterns, lets you access the previous value without it being tracked, and makes Signals consistent with State.
It means that when functions are stored in signals you need to use this form to remove ambiguity
const [count, setCount] = createSignal(ComponentA);
// Do this:
setCount(() => ComponentB);
// Don't do this as it will call the function immediately:
setCount(ComponentB);
https://github.com/solidjs/solid/blob/main/documentation/api.md#createsignal
createState
moved and renamed
createState
has been renamed to createStore
and moved to solid-js/store
. Also moved to solid-js/store
: createMutable
, produce
, reconcile
https://github.com/solidjs/solid/blob/main/documentation/api.md#createstore
SSR Entry points
renderToString
and renderToStringAsync
now only return their stringified markup. To insert scripts you need to call generateHydrationScript
or use the new <HydrationScript>
component.
renderToNodeStream
and renderToWebStream
have been replaced with pipeToNodeWritable
and pipeToWritable
, respectively.
Options Objects
Most non-essential arguments on reactive primitives are now living on an options object. This was done to homogenize the API and make it easier to make future additions while remaining backwards compatible.
on
No longer uses rest parameters for multiple dependencies. Instead, pass an array. This facilitates new option to defer execution until dependencies change.
https://github.com/solidjs/solid/blob/main/documentation/api.md#on
Actions renamed to Directives
To remove future confusion with other uses of actions the JSX.Actions
interface is now the JSX.Directives
interface.
https://github.com/solidjs/solid/blob/main/documentation/api.md#use___