S
SolidJS3mo ago
Ciantic

How to get 'client side' createResource behavior in SolidStart?

I noticed that if I don't have Suspense component in SolidStart, the whole page refreshes and createResource loading state doesn't render at all. For example if use this snippet from regular docs in solidstart project:
import { createSignal, createResource, Switch, Match, Show } from "solid-js";

const fetchUser = async (id) =>
{
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
}

function App() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>Loading...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>

</div>
);
}
import { createSignal, createResource, Switch, Match, Show } from "solid-js";

const fetchUser = async (id) =>
{
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
}

function App() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>Loading...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>

</div>
);
}
It will not show loading text at all, just the whole page refreshes when typing to input. This is quiet confusing when coming from normal SolidJS, that createResource loading etc are now useless. Thanks!
38 Replies
Brendonovich
Brendonovich3mo ago
it might be because start lazy loads routes which changes some of the async behaviours, best to just wrap your whole app in Suspense so that it's consistent
Ciantic
CianticOP3mo ago
@Brendonovich how would you do the snippet I pasted above? If I wrap whole thing in Suspense then the input still disappears when typing to input IMO, createResource cannot be used for interactivity anymore there needs to be some createClientResource or something
Brendonovich
Brendonovich3mo ago
because user() will re-suspend each time it refetches
Ciantic
CianticOP3mo ago
import { createSignal, createResource, Switch, Match, Show, Suspense } from "solid-js";

const fetchUser = async (id: any) => {
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<Suspense fallback={<div>Loading...</div>}>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>This is not doing anything, just for example...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</Suspense>
);
}
import { createSignal, createResource, Switch, Match, Show, Suspense } from "solid-js";

const fetchUser = async (id: any) => {
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<Suspense fallback={<div>Loading...</div>}>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>This is not doing anything, just for example...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</Suspense>
);
}
Brendonovich
Brendonovich3mo ago
you can use user.latest which doesn't suspend after the first run or wrap the setUserId call in startTransition so that user() shows the previous value while fetching the next one
Ciantic
CianticOP3mo ago
Okay I'm trying to understand, however I think the docs need updating, if some of the examples don't work as intended with SolidStart where would I put the user.latest I also tried:
onInput={(e) => startTransition(() => setUserId(e.currentTarget.value))}
onInput={(e) => startTransition(() => setUserId(e.currentTarget.value))}
I don't think it works either I like that SolidStart can be used to hydrate, but breaking createResource beyond recognition seems like too much to take at once. We need some way to toggle between old behavior for createResource and new
Brendonovich
Brendonovich3mo ago
Use .latest instead of calling user
Ciantic
CianticOP3mo ago
I see like this:
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
then it refreshes whole page on first change of input but subsequent change works Code looks like this at the moment:
import {
createSignal,
createResource,
Switch,
Match,
Show,
Suspense,
startTransition,
} from "solid-js";

const fetchUser = async (id: any) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>This is not doing anything, just for example...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
</Switch>
</div>
);
}
import {
createSignal,
createResource,
Switch,
Match,
Show,
Suspense,
startTransition,
} from "solid-js";

const fetchUser = async (id: any) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>This is not doing anything, just for example...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
</Switch>
</div>
);
}
screen goes white for 2 seconds on first change of input but after that it works reactively I think it is easier to make own createResource
Brendonovich
Brendonovich3mo ago
Oh because nothing would have been fetched on initial load Try providing an initialValue to the createResource
Ciantic
CianticOP3mo ago
I'm cooking my own createClientResource I don't yet understand this whole thing
Brendonovich
Brendonovich3mo ago
The behaviour that you’re observing is how createResource is supposed to work, it’s expected that there is a Suspense boundary regardless of Start Or even easier just set a default value for userId Create the signal with an empty string
Ciantic
CianticOP3mo ago
I'm not sure about this, why would docs have this exact example, that is so broken?
Brendonovich
Brendonovich3mo ago
Though that’ll suspend the page on initial load
Brendonovich
Brendonovich3mo ago
That example is super barebones, are you doing it in app.tsx or in a route?
Ciantic
CianticOP3mo ago
Im doing it in my own component that is added to index.tsx of page in solidstart
Brendonovich
Brendonovich3mo ago
So in a route?
Ciantic
CianticOP3mo ago
import { Title } from "@solidjs/meta";
import Counter from "~/components/Counter";
import { TestPages } from "~/components/TestPages";
import { TestFetch } from "~/components/TestFetch";

export default function Home() {
return (
<main>
<Title>Hello World</Title>
<h1>Hello world!</h1>
<Counter />
<TestFetch />
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
);
}
import { Title } from "@solidjs/meta";
import Counter from "~/components/Counter";
import { TestPages } from "~/components/TestPages";
import { TestFetch } from "~/components/TestFetch";

export default function Home() {
return (
<main>
<Title>Hello World</Title>
<h1>Hello world!</h1>
<Counter />
<TestFetch />
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
);
}
that TestFetch is my component with that createResource example
Brendonovich
Brendonovich3mo ago
Yeah I’m just wondering if you’re doing so in a route or in the root level app.tsx
Ciantic
CianticOP3mo ago
that is index.tsx from solidstart template
Brendonovich
Brendonovich3mo ago
Ok so probably route
Ciantic
CianticOP3mo ago
routes/index.tsx
Brendonovich
Brendonovich3mo ago
That basic example assumes there’s no lazy loading, no suspense, super barebones. Id guess that start’s lazy loading messes with resources when no Suspense is present Note that the example isn’t part of the start docs, it’s part of the plain solid docs
Ciantic
CianticOP3mo ago
yeah, I know 🙂 that's the whole premise, I'm coming from solidjs and trying to use solidstart
Brendonovich
Brendonovich3mo ago
Provide an initialValue to the createResource and it should work as you expect
Ciantic
CianticOP3mo ago
this also works:
function createClientResource(source: any, fetcher: any) {
const [result, setResult] = createSignal();
const [loading, setLoading] = createSignal(false);

createEffect(() => {
const v = source();
if (v) {
// @ts-ignore
setLoading(true);
fetcher(v).then((d: any) => {
// @ts-ignore
setLoading(false);
setResult(d);
});
}
});

return [result as any, loading as any];
}

export function TestFetch() {
const [userId, setUserId] = createSignal();
// const [user] = createResource(userId, fetchUser);
const [user, loading] = createClientResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={loading()}>
<p>I'm LOADING!...</p>
</Show>
<Switch>
{/* <Match when={user.error}>
<span>Error: {user.error}</span>
</Match> */}
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</div>
);
}
function createClientResource(source: any, fetcher: any) {
const [result, setResult] = createSignal();
const [loading, setLoading] = createSignal(false);

createEffect(() => {
const v = source();
if (v) {
// @ts-ignore
setLoading(true);
fetcher(v).then((d: any) => {
// @ts-ignore
setLoading(false);
setResult(d);
});
}
});

return [result as any, loading as any];
}

export function TestFetch() {
const [userId, setUserId] = createSignal();
// const [user] = createResource(userId, fetchUser);
const [user, loading] = createClientResource(userId, fetchUser);

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={loading()}>
<p>I'm LOADING!...</p>
</Show>
<Switch>
{/* <Match when={user.error}>
<span>Error: {user.error}</span>
</Match> */}
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</div>
);
}
Brendonovich
Brendonovich3mo ago
Can’t say I’m a fan of it but whatever works
Ciantic
CianticOP3mo ago
I don't think I like it either 😄 I provided initialValue but it still blinks
import {
createSignal,
createResource,
Switch,
Match,
Show,
Suspense,
startTransition,
createEffect,
} from "solid-js";

const fetchUser = async (id: any) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser, {
initialValue: 1,
});

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>I'm LOADING!...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</div>
);
}
import {
createSignal,
createResource,
Switch,
Match,
Show,
Suspense,
startTransition,
createEffect,
} from "solid-js";

const fetchUser = async (id: any) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
};

export function TestFetch() {
const [userId, setUserId] = createSignal();
const [user] = createResource(userId, fetchUser, {
initialValue: 1,
});

return (
<div>
<input
type="number"
min="1"
placeholder="Enter Numeric Id"
onInput={(e) => setUserId(e.currentTarget.value)}
/>
<Show when={user.loading}>
<p>I'm LOADING!...</p>
</Show>
<Switch>
<Match when={user.error}>
<span>Error: {user.error}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</div>
);
}
I forgot user.latest Okay! It works with initialValue and user.latest hack
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
<Match when={user.latest}>
<div>{JSON.stringify(user.latest)}</div>
</Match>
Thanks @Brendonovich I now have to think how all of this works
Brendonovich
Brendonovich3mo ago
user() will suspend whenever a new fetch happens, user.latest will suspend on only the first fetch and only if there isn’t a default
Ciantic
CianticOP3mo ago
solidstart handles suspends somehow differently from regular old solidjs
Brendonovich
Brendonovich3mo ago
did you have a Suspense in the non-start version? if you did i think it'd behave the same
Ciantic
CianticOP3mo ago
I don't think I did inside router or startserver there is suspense?
Brendonovich
Brendonovich3mo ago
No, but routes do get lazy loaded which could mess with things Try doing that same example in app.tsx instead
Ciantic
CianticOP3mo ago
hey! it works in app.tsx
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
import { TestFetch } from "./components/TestFetch";

export default function App() {
return <TestFetch />;
}
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
import { TestFetch } from "./components/TestFetch";

export default function App() {
return <TestFetch />;
}
When I use the original code inside TestFetch
Brendonovich
Brendonovich3mo ago
Yeah checks out
Ciantic
CianticOP3mo ago
and I see, the original app.tsx has suspense
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";

export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<a href="/">Index</a>
<a href="/about">About</a>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";

export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<a href="/">Index</a>
<a href="/about">About</a>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}
that is solidstart's app.tsx it has already Suspense
Brendonovich
Brendonovich3mo ago
Ah yeah the non-bare templates do
Ciantic
CianticOP3mo ago
If I remove it, SolidStart freaks out
Want results from more Discord servers?
Add your server