Skip to content

v1.5.0 - Batch to the Future

Compare
Choose a tag to compare
@ryansolid ryansolid released this 26 Aug 08:44
· 359 commits to main since this release

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