Data loading

What's a modern app without some data to power it? SolidStart aims to make it easy to load data from your data sources. It will help you keep your UI updated with your data. For most of your data requirements, you will likely be using the route to decide what data to load.

The URL is the primary way of navigating around your app. SolidStart has nested routing to help structure your app's UI in a hierarchical way, so that you can share layouts.

Solid has a createResource primitive that takes an async function and returns a signal from it. It's a great starting place for your data needs. It integrates with Suspense and ErrorBoundary to help you manage your lifecycle. Let's take a look at how we can use this to load data from a third party API for our app.

tsx
import { For, createResource } from "solid-js";
 
type Student = { name: string; house: string };
 
export default function Page() {
const [students] = createResource(async () => {
const response = await fetch("https://hogwarts.deno.dev/students");
return (await response.json()) as Student[];
});
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}
tsx
import { For, createResource } from "solid-js";
 
type Student = { name: string; house: string };
 
export default function Page() {
const [students] = createResource(async () => {
const response = await fetch("https://hogwarts.deno.dev/students");
return (await response.json()) as Student[];
});
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}

However, fetching inside your components can cause unnecessary waterfalls especially when nested under lazy loaded sections. To solve that it can be valuable to introduce a hoist and cache mechanism.

Libraries like Tanstack Query enable this but for the example below we will be using the data in APIs in @solidjs/router.

/routes/students.tsx
tsx
import { For } from "solid-js";
import { createAsync, cache } from "@solidjs/router";
 
type Student = { name: string; house: string };
 
const getStudents = cache(async () => {
const response = await fetch("https://hogwarts.deno.dev/students");
return (await response.json()) as Student[];
}, "students");
 
export const route = {
load: () => getStudents()
};
 
export default function Page() {
const students = createAsync(() => getStudents());
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}
/routes/students.tsx
tsx
import { For } from "solid-js";
import { createAsync, cache } from "@solidjs/router";
 
type Student = { name: string; house: string };
 
const getStudents = cache(async () => {
const response = await fetch("https://hogwarts.deno.dev/students");
return (await response.json()) as Student[];
}, "students");
 
export const route = {
load: () => getStudents()
};
 
export default function Page() {
const students = createAsync(() => getStudents());
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}

Caveats:

  1. The load function is only called once per route, the first time the user comes to that route. After that, the fine-grained resources that remain alive synchronize with state/url changes to refetch data when needed. If you need to refresh the data, you can use the refetch function that is returned by createResource.
  2. The load function is called before the route is rendered. It doesn't share the same context as the route. The context tree that is exposed to the load function is anything above the Page component.
  3. The load function will be called both on the server and the client. It's the resources that can avoid refetching if they had serialized their data in the server render.
  4. The server-side render will only wait for the resources to fetch and serialize if the resource signals are accessed under a Suspense boundary.

Data loading always on the server

The primary advantage of being a full-stack Javascript framework is that its easy to write data loading code that can run both on the server and client. SolidStart gives them superpowers. You might want to write code that only runs on your server but didn't want to create an API route for it.

It could be database access, or internal APIs, etc. It could sit within your functions where you need to use your server. We use "use server"; for this. It's a special comment that tells the bundler to create an RPC and not include the code in the client bundle .

/routes/students.tsx
tsx
import { For } from "solid-js";
import { createAsync, cache } from "@solidjs/router";
 
type Student = { name: string; house: string };
 
const getStudents = cache(async () => {
"use server";
return hogwarts.students.list();
}, "students");
 
export const route = {
load: () => getStudents()
};
 
export default function Page() {
const students = createAsync(() => getStudents());
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}
/routes/students.tsx
tsx
import { For } from "solid-js";
import { createAsync, cache } from "@solidjs/router";
 
type Student = { name: string; house: string };
 
const getStudents = cache(async () => {
"use server";
return hogwarts.students.list();
}, "students");
 
export const route = {
load: () => getStudents()
};
 
export default function Page() {
const students = createAsync(() => getStudents());
 
return <ul>{students() && students()!.map(student => <li>{student.name}</li>)}</ul>;
}