Skip to content

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)

TanStack Query Docs

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.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Deep Dive Into How Signals Work In SolidJS cover image

Deep Dive Into How Signals Work In SolidJS

SolidJS and Qwik have shown the world the power of signals, and Angular is following suit. There’s no way around them, so let's see what they are, why one would use them, and how they work. Signal basics Signals are built using the observer pattern. In this pattern, a subject holds a list of observers who are subscribed to changes to the subject. Whenever the subject gets changed, all subscribers will receive a notification of the update. Typically through a registered callback method. Observers may push new changes to that subject or other subjects. Triggering another set of updates throughout the observers. > From the above, you might have guessed that infinite loops are a big caveat with using the observer pattern. The same holds true for signals. The power of this pattern lies in the separation of concerns. Observers need to know little about the subject, except that it can change. Whichever actor is going to change the subject needs to know nothing about the observers. This makes it easy to build standalone services that know only about their domain. Signals in front-end frameworks SolidJS brings the observer pattern to the table for front-end frameworks through signals. Observers can be added to a signal through SolidJS’s concept of Effects (by using createEffect`). Components within SolidJS can be seen as both an observer and a subject at the same time. The component subscribes to all signals that are used to render the components HTML. SolidJS’s rendering system, in turn, is subscribed to all components. Where the components act as subjects and SolidJS as an observer. So whenever a signal changes in a component, the component reacts by changing its output, which then triggers SolidJS to put the new output on the screen. Compare this to, for example, Vue or React. When a change to the state occurs, the new values are passed down the component hierarchy. Each component returns its new output, which can be either the same as the previous render or changed. The framework then compares this tree against what it already had, and determines which parts to update. This is more dependent on a single source of truth which needs to know about all the components in the system. Changes are loosely related to each other, and only by diffing the results can the next step be determined. This differs from SolidJS setup, which makes hard connections between changes and results, making what will get updated when a signal changes more straightforward. Writing our own Signals At first sight, signals seem magical, and one might be inclined to believe there are some compiler tricks going on. Yet it is all plain JavaScript, and in this article, we’ll demystify signals in order to use them to their full potential. We can create our own signals with pure JavaScript in less than 25 lines. > Our simple version will not take objects or arrays as values as these are references in JavaScript and require special attention. Let's start with the interface. We want the signal creator, which is a function that returns a tuple with the first value being the getter and the second value the setter. The function accepts a value, which will be used as the initial value. This gives us: `JavaScript function createSignal(initialValue) { let value = initialValue; const getter = () => value; const setter = (newValue) => { value = newValue; }; return [getter, setter]; } const original = 1; const [count, setCount] = createSignal(original); console.log('Current count: ', count()); // Expected outcome: “Current count: 1” setCount(2); console.log('And now it is', count()); // Expected outcome: “And now it is 2” console.log('The original is the same', original); // Expected outcome: “The original is the same 1” ` > Note that, due to the fact that we created a new variable within the closure of our createSignal`, the variable outside of its scope will not change. `original` on the last line will still be “1”. For simplicity, we’re going to leave objects and arrays out of the picture as these are references instead of scalar values, and need extra code to do the same thing. Now that we can read from and write to our signal (a.k.a. subject), we’ll need to add subscribers to it. Whenever the getter is called (i.e., the value is read), we want the originator of the call to be registered as an observer. Then, when the setter is called, we are going to loop over all subscribed observers, and notify them of the new value. Consider this fully working signal creator. We’re almost there. `JavaScript function createSignal(initialValue) { let value = initialValue; const observers = []; const getter = (current) => { if (current && !observers.includes(current)) { observers.push(current); } return value; }; const setter = (newValue) => { value = newValue; observers.forEach((fn) => fn()); }; return [getter, setter]; } ` This snippet has a downside. It needs the observer to be passed as the argument to the getter. But we don’t want to deal with that. Our interface was to read signal()`, and have some sort of magic register the observer for us. What comes next was an eye-opener for me. I always believed there was some closure trick, or built-in JavaScript function to retrieve parent closures. That would have been a fantastic way to get who called the getter function and register it as an observer. But JavaScript offers nothing to support us in this. Instead, a way more simple trick is used, and it is seemingly used in every major framework. Frameworks, among others, React and SolidJS, store the parent in a global variable. Because JavaScript is single-threaded, it needs to execute all operations in order. It does a lot under the hood to get stuff like async to work. Clever developers have relied on this single-threaded aspect by writing to a global variable, and reading from it in the next function. This gets a little abstract, so here’s a concrete example to demonstrate this setup. `JavaScript let current; function first() { console.log(‘we are in first’); } function second() { current(); // set to function first before calling function second console.log(‘we are in second’); current = undefined; // clear it out, we’ve used it and don’t want it to pollute } function third() { if (current === undefined) { console.log(‘there is no current’); } } current = first; second(); third(); // Expected output // we are in first // we are in second // there is no current ` We do not need to worry about current` getting overwritten, as the code will always execute in order. It’s safe to assume that it will have the value we expect it to have when the body of `second` is executed. Note that we clear the value in the body of `first` as we don’t want unwanted side-effects by leaving the variable set. The Fully Working Signal Let’s add effects to our signals to complete the minimal signal minimal framework. With what we have learned in the previous section, we can create effects by Registering the effect callback (i.e., the observer) to our global variable current` Calling the observer for the first time. This will read all signals it depends upon, therefore adding current` to the observer list. Clearing current` to prevent registering the observer to signals read in the future. For this, we remove the current` argument from the getter, as this is now globally readable. And we can add the `createEffect` function. `JavaScript function createEffect(fn) { current = fn; fn(); current = undefined; } ` With this setup, we already have a working signal system. We can register effects and write out signals to trigger them. `JavaScript const [isSuccess, setSuccess] = setSignal(false); createEffect(() => console.log(‘We have ‘, isSuccess() ? ‘success!’ : ‘no success yet…’); // The above line will log “We have no success yet…” setSuccess(true); // Expected result: We have success! ` And there we have it! Working signals in just a couple of lines, no magic, no difficult JavaScript API. All just plain code. You can play around with the fully working example in this StackBlitz project. It has the signal setup as described, plus an example of stores. Run node index.js` to see the result. This simple framework is only focused on showcasing signals. For demonstration purposes, it simply logs to the console. Frameworks like SolidJS have advanced effects and logic to get HTML rendering to work. If you’re interested in learning more about rendering, you can read Mark’s blog on how to create your own custom renderer in SolidJS Or put your newly learned skills to use in a new SolidJS project created with Starter.dev’s SolidJS and Tailwind starter!...

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights) cover image

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights)

We take accessibility quite seriously here at This Dot because we know it's important. Still, throughout my career, I've seen many projects where accessibility was brushed aside for reasons like "our users don't really use keyboard shortcuts" or "we need to ship fast; we can add accessibility later." The truth is, that "later" often means "never." And it turns out, anyone could break their hand, like I did. I broke my dominant hand and spent four weeks in a cast, effectively rendering it useless and forcing me to work left-handed. I must thus apologize for the misleading title; this post should more accurately be dubbed "second-hand" accessibility insights. The Perspective of a Developer Firstly, it's not the end of the world. I adapted quickly to my temporary disability, which was, for the most part, a minor inconvenience. I had to type with one hand, obviously slower than my usual pace, but isn't a significant part of a software engineer's work focused on thinking? Here's what I did and learned: - I moved my mouse to the left and started using it with my left hand. I adapted quickly, but the experience wasn't as smooth as using my right hand. I could perform most tasks, but I needed to be more careful and precise. - Many actions require holding a key while pressing a mouse button (e.g., visiting links from the IDE), which is hard to do with one hand. - This led me to explore trackpad options. Apart from the Apple Magic Trackpad, choices were limited. As a Windows user (I know, sorry), that wasn't an option for me. I settled for a cheap trackpad from Amazon. A lot of tasks became easier; however, the trackpad eventually malfunctioned, sending me back to the mouse. - I don't know a lot of IDE shortcuts. I realized how much I've been relying on a mouse for my work, subconsciously refusing to learn new keyboard shortcuts (I'll be returning my senior engineer license shortly). So I learned a few new ones, which is good, I guess. - Some keyboard shortcuts are hard to press with one hand. If you find yourself in a similar situation, you may need to remap some of them. - Copilot became my best friend, saving me from a lot of slow typing, although I did have to correct and rewrite many of its suggestions. The Perspective of a User As a developer, I was able to get by and figure things out to be able to work effectively. As a user, however, I got to experience the other side of the coin and really feel the accessibility (or lack thereof) on the web. Here are a few insights I gained: - A lot of websites apparently tried_ to implement keyboard navigation, but failed miserably. For example, a big e-commerce website I tried to use to shop for the aforementioned trackpad seemed to work fine with keyboard navigation at first, but once I focused on the search field, I found myself unable to tab out from it. When you make the effort to implement keyboard navigation, please make sure it works properly and it doesn't get broken with new changes. I wholeheartedly recommend having e2e tests (e.g. with Playwright) that verify the keyboard navigation works as expected. - A few websites and web apps I tried to use were completely unusable with the keyboard and were designed to be used with a mouse only. - Some sites had elaborate keyboard navigation, with custom keyboard shortcuts for different functionality. That took some time to figure out, and I reckon it's not as intuitive as the designers thought it would be. Once a user learns the shortcuts, however, it could make their life easier, I suppose. - A lot of interactive elements are much smaller than they should be, making it hard to accurately click on them with your weaker hand. Designers, I beg you, please make your buttons bigger. I once worked on an application that had a "gloves mode" for environments where the operators would be using gloves, and I feel like maybe the size we went with for the "gloves mode" should be the standard everywhere, especially as screens get bigger and bigger. - Misclicking is easy, especially using your weaker hand. Be it a mouse click or just hitting an Enter key on accident. Kudos to all the developers who thought about this and implemented a confirmation dialog or other safety measures to prevent users from accidentally deleting or posting something. I've however encountered a few apps that didn't have any of these, and those made me a bit anxious, to be honest. If this is something you haven't thought about when developing an app, please start doing so, you might save someone a lot of trouble. Some Second-Hand Insights I was only a little bit impaired by being temporarily one-handed and it was honestly a big pain. In this post, I've focused on my anecdotal experience as a developer and a user, covering mostly keyboard navigation and mouse usage. I can only imagine how frustrating it must be for visually impaired users, or users with other disabilities, to use the web. I must confess I haven't always been treating accessibility as a priority, but I've certainly learned my lesson. I will try to make sure all the apps I work on are accessible and inclusive, and I will try to test not only the keyboard navigation, ARIA attributes, and other accessibility features, but also the overall experience of using the app with a screen reader. I hope this post will at least plant a little seed in your head that makes you think about what it feels like to be disabled and what would the experience of a disabled person be like using the app you're working on. Conclusion: The Humbling Realities of Accessibility The past few weeks have been an eye-opening journey for me into the world of accessibility, exposing its importance not just in theory but in palpable, daily experiences. My short-term impairment allowed me to peek into a life where simple tasks aren't so simple, and convenient shortcuts are a maze of complications. It has been a humbling experience, but also an illuminating one. As developers and designers, we often get caught in the rush to innovate and to ship, leaving behind essential elements that make technology inclusive and humane. While my temporary disability was an inconvenience, it's permanent for many others. A broken hand made me realize how broken our approach towards accessibility often is. The key takeaway here isn't just a list of accessibility tips; it's an earnest appeal to empathize with your end-users. "Designing for all" is not a checkbox to tick off before a product launch; it's an ongoing commitment to the understanding that everyone interacts with technology differently. When being empathetic and sincerely thinking about accessibility, you never know whose life you could be making easier. After all, disability isn't a special condition; it's a part of the human condition. And if you still think "Our users don't really use keyboard shortcuts" or "We can add accessibility later," remember that you're not just failing a compliance checklist, you're failing real people....