How to refetch a "use server" function / RPC?

I've created a server function that I can confirm works as expected, now I'm trying to figure out how to "correctly" re-run this function on a timed interval. Using the documentation (https://start.solidjs.com/core-concepts/data-loading), it first mentions createResource() for requests, which makes sense since it comes with a mutate and refetch function. Then it goes on to give a "Data loading always on the server" example, now moving to createAsync and cache (both /router imports), but from my attempts to use those examples, they work for initial page load but do not appear to contain mechanisms for re-running the server function and updating the UI accordingly. Additionally, the example listed above doesn't seem to actually run anything async (there's no await in that async server function), so I'm wondering if maybe the example is still missing something (like when is the route object used?! Is it a magic object / used somehow when exported from a page? Not listed in API Routes tab). Am I missing something? Thanks! (Code examples in thread)
SolidStart Beta Documentation
SolidStart Beta Documentation
Early release documentation and resources for SolidStart Beta
63 Replies
HashtagOctothorp
Initial example using the createAsync example from the docs:
async function pingServers() {
"use server";
let status = await Monitor.run(); // this is a class that runs a node.js module `tcp-ping` and returns a map of servers and their ping-able status
console.log(status); // see this log in the server console
return status;
}

const cachedPing = cache(pingServers, "mycache")

export default function Home() {
const status = createAsync(cachedPing)
// What can I do to re-fetch this clientside with a setTimeout / setInterval?

return (
<main class="mx-auto">
<pre class="my-4">
{JSON.stringify(status(), null, 2)}
</pre>
</main>
);
}
async function pingServers() {
"use server";
let status = await Monitor.run(); // this is a class that runs a node.js module `tcp-ping` and returns a map of servers and their ping-able status
console.log(status); // see this log in the server console
return status;
}

const cachedPing = cache(pingServers, "mycache")

export default function Home() {
const status = createAsync(cachedPing)
// What can I do to re-fetch this clientside with a setTimeout / setInterval?

return (
<main class="mx-auto">
<pre class="my-4">
{JSON.stringify(status(), null, 2)}
</pre>
</main>
);
}
My attempt to use createResource instead, running into weird problems, see comments:
async function pingServers() {
"use server";
let status = await Monitor.run(); // this is a class that runs a node.js module `tcp-ping` and returns a map of servers and their ping-able status
console.log(status); // see this log in the server console
return status;
}

/*
Initial page load has the expected results from the RCP call,
but the setTimeout refetch() causes a "TypeError Failed to fetch" with the following browser call stack UI:
createRequest server-runtime.jsx:85:10
fetchServerFunction server-runtime.jsx:99:123
(and WHY is the displayed callstack set to user-select: none?!)

Note: The Server console logs the expected status in the pingServers() function before failing with the following message:
file:///C:/Workspace/home-services-start/node_modules/solid-js/dist/server.js:525
ctx.resources[id].data = res;
^
TypeError: Cannot set properties of undefined (setting 'data')
*/
export default function Home() {
const [status, { mutate, refetch}] = createResource(pingServers)
setTimeout(() => {
console.log("refetching status")
refetch()
}, 5000)
return (
<main class="mx-auto">
<pre class="my-4">
{JSON.stringify(status(), null, 2)}
</pre>
</main>
);
}
async function pingServers() {
"use server";
let status = await Monitor.run(); // this is a class that runs a node.js module `tcp-ping` and returns a map of servers and their ping-able status
console.log(status); // see this log in the server console
return status;
}

/*
Initial page load has the expected results from the RCP call,
but the setTimeout refetch() causes a "TypeError Failed to fetch" with the following browser call stack UI:
createRequest server-runtime.jsx:85:10
fetchServerFunction server-runtime.jsx:99:123
(and WHY is the displayed callstack set to user-select: none?!)

Note: The Server console logs the expected status in the pingServers() function before failing with the following message:
file:///C:/Workspace/home-services-start/node_modules/solid-js/dist/server.js:525
ctx.resources[id].data = res;
^
TypeError: Cannot set properties of undefined (setting 'data')
*/
export default function Home() {
const [status, { mutate, refetch}] = createResource(pingServers)
setTimeout(() => {
console.log("refetching status")
refetch()
}, 5000)
return (
<main class="mx-auto">
<pre class="my-4">
{JSON.stringify(status(), null, 2)}
</pre>
</main>
);
}
Brendonovich
Brendonovich7mo ago
Invalidating cachedPing will cause the cache to refetch and run the server function
HashtagOctothorp
How do I do that? Is there any more documentation for cache and createAsync?
Brendonovich
Brendonovich7mo ago
Check the docs:
You can revalidate the cache using the revalidate method or you can set revalidate keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the cache (ie "users" in the example above). You can also invalidate a single entry by using keyFor.
So import revalidate from solid-router and give it a cache key
HashtagOctothorp
I see, I was wondering where to find these kinds of specs. Linking them from the start.solidjs.com docs would probably be helpful. (Github readme's arent usually very high on the SEO)
Brendonovich
Brendonovich7mo ago
Probably, but they're not really anything to do with solid-start
HashtagOctothorp
Any reason why I'm getting the errors shown in the 2nd example? Based purely on the documentation for the associated calls (and even seeing the RCP run correctly before solid crashes), it looks like it should work
Brendonovich
Brendonovich7mo ago
Have you tried createResource(() => pingServers())?
lxsmnsyc
lxsmnsyc7mo ago
also be careful on setTimeout, that thing runs on the server
HashtagOctothorp
I tried wrapping it with a full function that basically did the same thing:
async function fetchData(source, { value, refetching }) {
return await pingServers()
}
...
const status = createResource(fetchData)
async function fetchData(source, { value, refetching }) {
return await pingServers()
}
...
const status = createResource(fetchData)
Though on a second look, your example was straight calling pingServers synchronously. I can try that Noted. Since I've confirmed that the invalidate() approach works, I've since turned it to a 1 minute interval that then invalidates every 15 minutes based on a "last fetched" Date.now() associated with the response. I assumed that this was client side, hence the interval starting at any point in the ideal "don't invalidate cache until 15 minutes has passed", but if this is running server-side too... does it create a new interval each time the page is refreshed?!
lxsmnsyc
lxsmnsyc7mo ago
it creates an interval every request. Better call this in a createEffect and also make sure to clean it up
HashtagOctothorp
Cleanup already done but I'll do the createeffect for clientside. Thanks Hmm... still seeing some odd behavior. RPC is run twice on each refresh (presumably once for SSR, once for client?), and the cache doesn't appear to be saved between page reloads. Is this an artifict of Dev mode (and I should test with npm run start instead of npm run dev, or something else not working as I would think it should? derp, found the source of the second rpc... my fault for uncommenting the createResource when trying to test @Brendonovich 's idea
lxsmnsyc
lxsmnsyc7mo ago
it's possible for it to run at least twice because of the Suspense mechanism so you might want to wrap the return of Home with a Suspense
HashtagOctothorp
Done, but that isn't changing the fact that it's reloading the RCP on each page reload (not caching like I would have assumed). Reading the cache docs now.
Brendonovich
Brendonovich7mo ago
By 'reload' do you mean refreshing the page or calling reload()?
HashtagOctothorp
refreshing the page
Brendonovich
Brendonovich7mo ago
Ah cache is just for the current session
HashtagOctothorp
gotcha
Brendonovich
Brendonovich7mo ago
it's not a persistent cache
HashtagOctothorp
global server state stuff... I think i saw something about how that's not recommended it's not a big deal, this is mostly only running at home
Brendonovich
Brendonovich7mo ago
From the docs:
This cache accomplishes the following: It does just deduping on the server for the lifetime of the request. It does preload cache in the browser which lasts 10 seconds. When a route is preloaded on hover or when load is called when entering a route it will make sure to dedupe calls. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.
HashtagOctothorp
if it was going to scale to 100's or 1000s of users, I'd def need a more performant mechanism, but that would also probably mean a Redis cache somewhere in there.
Brendonovich
Brendonovich7mo ago
I doubt 1000s of users would require a server-side cache, but even then it's got nothing to do with the purpose of solid router's cache
HashtagOctothorp
1000s of users all triggering tcp pings to check server status? I could see that being an issue.
Brendonovich
Brendonovich7mo ago
If that's all they're doing I think you may underestimate how powerful computers are 😅
HashtagOctothorp
perhaps I'm maybe overarchitecting too thansk for the help
Brendonovich
Brendonovich7mo ago
One machine can power tens or hundreds of thousands of connections (depending on what you build with), just focus on making something that works
HashtagOctothorp
My comments about the SolidStart docs still stand though. The Server Only example is confusing.
Brendonovich
Brendonovich7mo ago
Which one was that? Oh also does createResource(() => pingServers()) work
HashtagOctothorp
Link in OP (Checking) No. Wait... yes, but refetch causes a flash / reload of the page (scrolls back to top). (Maybe a side effect of Suspense? Going to try that now) const [status, { mutate, refetch}] = createResource(() => pingServers()) Not caused by suspense
Brendonovich
Brendonovich7mo ago
I ask bc my guess with this one is that pingServers was being called with arguments from the createResource which may have been causing a problem
HashtagOctothorp
pingServers doesn't take arguments though so sending some wouldn't do anything (shouldn't)
Brendonovich
Brendonovich7mo ago
Well yeah idk if it will or not but I do know that createResource will give it arguments
No description
HashtagOctothorp
Like I said, I had tried
async function fetchData(source, { value, refetching }) {
return await pingServers()
}
...
const [status, { mutate, refetch}] = createResource(fetchData)
async function fetchData(source, { value, refetching }) {
return await pingServers()
}
...
const [status, { mutate, refetch}] = createResource(fetchData)
But this caused the same error as sending PingServers directly. (createResource(pingServers)) The main difference in your suggestion was not having an async function but fundamentally returning the promise instead of the result to createResource which might explain why it worked, but also it's creating a new promise every time (potentially causing the flash?)
Brendonovich
Brendonovich7mo ago
Well yours is also creating a new promise each time
lxsmnsyc
lxsmnsyc7mo ago
if it's the entire page, have you followed my Suspense suggestion?
HashtagOctothorp
if you mean wrap the return(<main>...</main>) with a <Suspence> tag like return(<Suspence><main>...</main></Suspence>), yes I did (had already added it when I tried @Brendonovich's suggestion, then also tried without) I suppose that's true, but doesn't explain why an async/await function wrapper (or sending the server async function directly) causes the stack error, but a function that isn't async works... Continuing this thread instead of creating a new one: I'm trying to pipe the results of the createAsync into a Context provider so I don't have to prop-drill it. So far I am only getting undefined for the result of the contextProvider, even though the
//route index
type Ping = Awaited<ReturnType<typeof pingServers>>
const PingContext = createContext<Ping>();
export const usePingContext = () => useContext(PingContext)
...
export default Home = () => {
...
return (
<PingContext.Provider value={status()}>
... the rest of the stuff
// referencing status() in this component works fine and updates as expected when the cache is invalidated
</PingContext.Provider>
)
}

// component file
import usePingContext
...
const MyComponent = ()=>{
const PingStatus = usePingContext()
return (
...
// PingStatus is always undefined
)
}
//route index
type Ping = Awaited<ReturnType<typeof pingServers>>
const PingContext = createContext<Ping>();
export const usePingContext = () => useContext(PingContext)
...
export default Home = () => {
...
return (
<PingContext.Provider value={status()}>
... the rest of the stuff
// referencing status() in this component works fine and updates as expected when the cache is invalidated
</PingContext.Provider>
)
}

// component file
import usePingContext
...
const MyComponent = ()=>{
const PingStatus = usePingContext()
return (
...
// PingStatus is always undefined
)
}
My other failed attempts were: * export the result of the cache and import in another file, using a second createAsync: export const cachedPing = cache(pingServers, "mycache") * move the createAsync out of the component function and export / import that instead Any tips?
Brendonovich
Brendonovich7mo ago
from the docs:
The value passed to provider is passed to useContext as is. That means wrapping as a reactive expression will not work. You should pass in Signals and Stores directly instead of accessing them in the JSX.
HashtagOctothorp
I also tried passing the accessor function itself (status) instead of the value (status()), and am getting the same result I need to create an effect to update a signal every time the createAsync changes?
Brendonovich
Brendonovich7mo ago
you'll need to do so either way
HashtagOctothorp
I guess I assumed that a createAsync would be a signal
Brendonovich
Brendonovich7mo ago
yea it is
HashtagOctothorp
Ok, went through the rest of the docs. If the result of createAsync is a signal, theoretically it should have worked to move that out of the component definition, allowing me to export and import it, no? (removing the need for Context?)
Brendonovich
Brendonovich7mo ago
technically yeah if you're using ssr you might get into some trouble but other than that should be fine would encourage trying to pass the whole signal down through context again tho
HashtagOctothorp
I get a "pingStatus is not defined" error when exporting export const pingStatus = createAsync(cachedPing) in my routes/index.tsx, and importing in my Component file
Brendonovich
Brendonovich7mo ago
hmm might be because you're exporting it from a route file
HashtagOctothorp
Should I swap, export in the component, import in the route? hmm that didn't work either Cannot use 'in' operator to search for 'data' in undefined in the SolidStart server.js file
Brendonovich
Brendonovich7mo ago
surely there's one of your own files in that stack trace
HashtagOctothorp
oh sure, that's just the last point of failure
Brendonovich
Brendonovich7mo ago
i find it's usually more useful to look at where errors were thrown in my own files rather than the internals
HashtagOctothorp
I'd copy the stack trace but it blows my mind that there's CSS styles prevent copying the text
Brendonovich
Brendonovich7mo ago
copy from cli? ah nah it's client side
HashtagOctothorp
output to the browser is different
Brendonovich
Brendonovich7mo ago
you're doing import { pingStatus } from "..." yeah?
HashtagOctothorp
yep
import {
PINGCACHE, // string value to share the cache name
Servers, // Root Component from this component
pingStatus, // the createAsync signal
} from "~/components/Servers";
import {
PINGCACHE, // string value to share the cache name
Servers, // Root Component from this component
pingStatus, // the createAsync signal
} from "~/components/Servers";
Brendonovich
Brendonovich7mo ago
¯\_(ツ)_/¯
HashtagOctothorp
Joy at this point... I'm about to just setup a separate API definition instead XD I feel like that would be easier which is unfortunate, as the whole point of going the RPC route was to prevent that lol. I tried one last thing, moving the cache definition to the same node file the RPC call is defined in, and NOW it works XD ¯\_(ツ)_/¯ maybe it was one of those recursive dependency problems (though I don't see why it would have been) ah, but of course now I'm getting errors saying that computations created outside a "createRoot" or "render" will never be disposed... because it's outside the Solid lifecycle 🤦‍♂️
HashtagOctothorp
seems like createAsync needs to be within the lifecycle of a component, is my guess yep, confirmed
lxsmnsyc
lxsmnsyc6mo ago
yes, createResource has that requirement, thanks to createUniqueId
HashtagOctothorp
createResource or createAsync?
Brendonovich
Brendonovich6mo ago
createResource, which createAsync is built from
HashtagOctothorp
Ah Wasn't clear from the docs I read. I feel like if I'm going to learn Solid proper, I need to be in the source