Skip to content

v1.4.0 - Level Up!

Compare
Choose a tag to compare
@ryansolid ryansolid released this 12 May 20:06
· 419 commits to main since this release

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.