Challenges of SSR with SolidStart and TanStack Query v4
Originally published at: https://www.thisdot.co/blog/challenges-of-ssr-with-solidstart-and-tanstack-query-v4/
Coming from developing in React, a lot of us are big fans of TanStack Query. It adds that layer for async data fetching to React we needed. So when shifting to a new framework, Solid, which has a familiar signature as React, we wanted to bring our beloved tools with us.
During the development of our showcase, we came to realize that the combination of TanStack Query (v4, v5 seems to include positive changes) and SolidStart was not meant to be.
Understanding the differences
Different interface
Right out of the box, the experience between Solid and React differs. There’s the first very obvious issue that the documentation for Solid consists of a single page, whereas React gets a full book on documentation.
But more important is the way one uses TanStack Query. React directly takes the tuple containing the query name and variables. Where Solid, due to the way reactivity works, needs a function returning the tuple. This way, Solid can bind an effect to the query to ensure it triggers when the dependencies change. It’s not a big difference, but it indicates that TanStack Query React and TanStack Query Solid are not the same.
// ❌ react version
useQuery([\"todos\", todo], fetchTodos)
// ✅ solid version
createQuery(() => [\"todos\", todo()], fetchTodos)
Stores
What is not so apparent from the documentation are the changes under the hood. React triggers rerenders when state changes are pushed. These rerenders will, in turn, compare the new variables against dependencies to determine what to run. This does not require special treatment of the state. Whatever data is passed to React will be used directly as is.
Solid, on the other hand, requires Signals to function. To save you the hassle, TanStack will create stores from the returned data for you. With the dependency tuple as a function and the return value as store, TanStack Query closes the reactivity loop. Whenever a signal changes, the query will be triggered and load new data. The new data gets written to the store, signalling all observers.
Why it doesn’t work
Solid comes prepacked with Resources. These basically fill the same functionality as TanStack Query offers. Although TanStack does offer more features for the React version. Resources are Signal wrappers around an async process. Typically they’re used for fetching data from a remote source.
Although both Resources and TanStack Query do the same thing, the different
signatures makes it so they’re not interchangeable. Resources have loading
where TanStack uses isLoading
.
SolidStart
SolidStart is an opinionated meta-framework build on top of SolidJS and Solid router. One of the features it brings to the table is Server-side rendering (SSR). This sends a fully rendered page to the client, as opposed to just sending the skeleton HTML and having the client build the page after the initial page load. With SSR, the server also send additional information to the client for SolidJS to hydrate and pick up where the server left off. This prevents the client from re-rendering all the work the server had already done.
In order for SSR to work, one needs to create pages. SolidStart offers a feature that allows developers to inject data into their pages. By doing so, one can set up a generic GUI for loading data when changing between pages. A very minimal example of this looks like:
export function routeData() {
const [count] = createSignal(4);
return count;
}
export defaulft function Page() {
const count = useRouteData();
return <p>The current count is {count()}</p>;
}
When combining this setup with routing and createResource
, there’s some
caveats that need to be taken into consideration. These are described in the
official
SolidStart docs. In
order to keep the routes maintainable, SolidStart offers createRouteData
that
simplifies the setup and to mitigate potential issues caused by misusing the
system.
createRouteData, resources and TanStack Query
It is with createRouteData
that we run into issues with combining SolidStart
and TanStack Query. In order to use SSR, SolidStart needs the developers to use
createRouteData
. Which in turn expects to create a resource for the async
operation that is required to load the page’s data.
By relying on a resource being returned, SolidStart can take control of the flow. It knows when it’s rendering on the server, how to pass both the HTML and the data to server, and finally how to pick up on the client.
As stated before, TanStack Query relies on stores, not on resources. Therefore
we cannot swap out createRouteData
and createQuery
even though they both
fill the same purpose. Our initial attempt was to wrap the returned data from
createQuery
to resemble the shape of a resource. But that started to throw
errors as soon as we tried to load a page.
Under the hood, both SolidStart and TanStack Query are doing their best to hold control over the data flow. Systems like caching, hydration strategies and refetching logic are running while it seems like we’re just fetching data and passing it to the render engine. These systems conflict (they both are trying to do the same thing and get stuck in a tug-o-war for the data). This results in a situation where we can either satisfy TanStack Query or SolidStar.
We can probably make it work by creating an advanced adapter that awaits and
pulls the data from a query. Use that data to create our own resource and feed
that to createRouteData
to have SolidStart do its thing. Our conclusion is
that there’s too much effort needed to create and maintain such an adapter
especially when taking into consideration that we can simply move away from
TanStack Query (for now) and use resources as SolidStart intents.