S
SolidJS4w ago
sean

How to use query.set (prev cache.set) to mutate async data for optimistic updates?

i’m trying to use the undocumented query.set to mutate async data for optimistic updates. it works for async data from createAsync that’s reactive to param change. however, when i manually revalidate the data (either by calling revalidate somewhere, or revalidating from an action), it fetches fresh data rather than using the query.set i called earlier when should i use query.set if i’m going use revalidate() in order get the data? calling revalidate is the only way to get the data as my query isn’t reactive to a param change i’m trying to do following
const data = createAsync(() => getData)
const createAction = action(async () => {
const newData = createData()
query.set(createData.key, newData)
return reload({ revalidate: createData.key })
})

const create = useAction(createAction)

await create()
const data = createAsync(() => getData)
const createAction = action(async () => {
const newData = createData()
query.set(createData.key, newData)
return reload({ revalidate: createData.key })
})

const create = useAction(createAction)

await create()
i’m expecting that it doesn’t create a new request, but rather use the data i set using query.set i’m using query.set to avoid fetching getData() again because i already got the same data when i called createData() is my usage correct? how do i properly use it to attain the behavior i described? (sorry for bad formatting, typing on a phone) thank you so much!!!
8 Replies
sean
seanOP4w ago
i checked other implementations here and they didn’t call revalidate() as that seems to revalidate the cache, not force query() to be re-run however, when i did try to query.set, the data didn’t get mutated. so i assumed query needed to be fired with this, how can i query.set and see my change reflect?
brenelz
brenelz4w ago
I think you want to use the onComplete hook on the action
sean
seanOP4w ago
i checked the docs, and i can’t seem to find the onComplete hook. perhaps it’s undocumented? do u have example usage? https://docs.solidjs.com/solid-router/reference/data-apis/action
peerreynders
peerreynders4w ago
For the "gold standard" guide to optimistic updates look at the Strello app. - The query that holds the inbound data from the server (that can be invalidated). - The createAsync in the board route that reads that query to obtain the board data and forwards it to the Board component via the board prop. - The boardStore that is loaded by the board prop and ends up driving the entire UI. It is this store that is modified to drive optimistic updates. - The effect that is only meant to run when the board prop updates; it's responsible for synchronizing boardStore whenever the query updates due to invalidations caused by actions. - The effect that is meant to run whenever there is a new submission by a board action in order to optimistically update boardStore. (For a variation on that take, see here) So the query feeds the store that is optimistically updated while still taking invalidation updates from the query.
GitHub
strello/src/components/Board.tsx at 9c9ae973d96cc045914e696757a1b5f...
Contribute to solidjs-community/strello development by creating an account on GitHub.
GitHub
strello/src/lib/index.ts at 9c9ae973d96cc045914e696757a1b5f31efc6fa...
Contribute to solidjs-community/strello development by creating an account on GitHub.
GitHub
strello/src/routes/board/[id].tsx at 9c9ae973d96cc045914e696757a1b5...
Contribute to solidjs-community/strello development by creating an account on GitHub.
peerreynders
peerreynders2d ago
Combining createWritableMemo with context.
// file: src/components/my-context.tsx
import { createContext, useContext } from 'solid-js';
import { createAsync, query } from '@solidjs/router';
import { createWritableMemo } from '@solid-primitives/memo';

import type { Accessor, ParentProps, Setter } from 'solid-js';

let last = 0;
async function myData() {
return last++;
}

const asyncData = query(myData, 'my-data');

type MyResult = Awaited<ReturnType<typeof myData>>;
type ContextType = {
override: Setter<MyResult | undefined>;
data: Accessor<MyResult | undefined>;
};

const MyContext = createContext<ContextType>();

function MyProvider(props: ParentProps) {
const remoteData = createAsync(() => asyncData());
const [data, override] = createWritableMemo(remoteData);
const context = {
override,
data,
};
return (
<MyContext.Provider value={context}>{props.children}</MyContext.Provider>
);
}

function useMyContext() {
const ctx = useContext(MyContext);
if (!ctx) throw new Error('MyContext not initialized');

return ctx;
}

const useOverride = () => useMyContext().override;

const useData = () => useMyContext().data;

export { MyProvider, useOverride, useData };
// file: src/components/my-context.tsx
import { createContext, useContext } from 'solid-js';
import { createAsync, query } from '@solidjs/router';
import { createWritableMemo } from '@solid-primitives/memo';

import type { Accessor, ParentProps, Setter } from 'solid-js';

let last = 0;
async function myData() {
return last++;
}

const asyncData = query(myData, 'my-data');

type MyResult = Awaited<ReturnType<typeof myData>>;
type ContextType = {
override: Setter<MyResult | undefined>;
data: Accessor<MyResult | undefined>;
};

const MyContext = createContext<ContextType>();

function MyProvider(props: ParentProps) {
const remoteData = createAsync(() => asyncData());
const [data, override] = createWritableMemo(remoteData);
const context = {
override,
data,
};
return (
<MyContext.Provider value={context}>{props.children}</MyContext.Provider>
);
}

function useMyContext() {
const ctx = useContext(MyContext);
if (!ctx) throw new Error('MyContext not initialized');

return ctx;
}

const useOverride = () => useMyContext().override;

const useData = () => useMyContext().data;

export { MyProvider, useOverride, useData };
Solid Primitives
A library of high-quality primitives that extend SolidJS reactivity
peerreynders
peerreynders2d ago
// file: src/routes/about.tsx
import { onMount, onCleanup } from 'solid-js';
import { revalidate } from '@solidjs/router';
import { Title } from '@solidjs/meta';
import { MyProvider, useOverride, useData } from '../components/my-context.js';

import type { ParentProps } from 'solid-js';

const INTERVAL = 1000;

function Middle(props: ParentProps) {
const override = useOverride();

let timer: ReturnType<typeof setTimeout>;
let start: number;
let pass = 1;
function cycle() {
if (pass % 2 === 0) {
// even pass - update through query (increment to next whole number)
revalidate('my-data');
} else {
// odd pass - update via override (increment by half)
override((current) => (current !== undefined ? current + 0.5 : current));
}
const targetNext = (pass + 1) * INTERVAL + start;
pass += 1;
timer = setTimeout(cycle, targetNext - performance.now());
}

onMount(() => {
start = performance.now();
timer = setTimeout(cycle, INTERVAL);
});
onCleanup(() => {
clearTimeout(timer);
});

return <>{props.children}</>;
}

function Leaf() {
const data = useData();

return <div class="increment mi-auto">{data()}</div>;
}

function About() {
return (
<MyProvider>
<main>
<Title>About</Title>
<h1>About</h1>
<Middle>
<Leaf />
</Middle>
</main>
</MyProvider>
);
}

export { About };
// file: src/routes/about.tsx
import { onMount, onCleanup } from 'solid-js';
import { revalidate } from '@solidjs/router';
import { Title } from '@solidjs/meta';
import { MyProvider, useOverride, useData } from '../components/my-context.js';

import type { ParentProps } from 'solid-js';

const INTERVAL = 1000;

function Middle(props: ParentProps) {
const override = useOverride();

let timer: ReturnType<typeof setTimeout>;
let start: number;
let pass = 1;
function cycle() {
if (pass % 2 === 0) {
// even pass - update through query (increment to next whole number)
revalidate('my-data');
} else {
// odd pass - update via override (increment by half)
override((current) => (current !== undefined ? current + 0.5 : current));
}
const targetNext = (pass + 1) * INTERVAL + start;
pass += 1;
timer = setTimeout(cycle, targetNext - performance.now());
}

onMount(() => {
start = performance.now();
timer = setTimeout(cycle, INTERVAL);
});
onCleanup(() => {
clearTimeout(timer);
});

return <>{props.children}</>;
}

function Leaf() {
const data = useData();

return <div class="increment mi-auto">{data()}</div>;
}

function About() {
return (
<MyProvider>
<main>
<Title>About</Title>
<h1>About</h1>
<Middle>
<Leaf />
</Middle>
</main>
</MyProvider>
);
}

export { About };
peerreynders
peerreynders18h ago
https://discord.com/channels/722131463138705510/722131463889223772/1331150634258268222
is there a way to make a signal, store, or this writable trigger Suspense while their source of truth (resource from createAsync) is still resolving?
The straightforward thing is to ensure that the createAsync that feeds into the create[Writable]Memo is under the Suspense boundary that you want to trigger; then it just works. So in the case of the context example place the MyProvider just under the Suspense boundary; the MyProvider's createAsync will trigger that boundary whenever appropriate.
the alternative is using Show instead of Suspense, but this doesn’t seem to be ideal
Show is suitable for synchronously reactive values, Suspense is a necessary evil for asynchronously discontinuous values. I assume that you want to artificially trigger Suspense for the sake of the fallback. Meanwhile useTransition exists largely to implement paint-holding which overrides the fallback anyway. In fact query makes liberal use of startTransition.
Chrome for Developers
Paint Holding - reducing the flash of white on same-origin navigati...
A quick overview of paint holding. A Chrome feature for reducing the flash of white on same-origin navigations
GitHub
solid-router/src/data/query.ts at 50c5d7bdef6acc5910c6eb35ba6a24b15...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
peerreynders
peerreynders18h ago
The thinking is that stale information is better than a component-level loading spinner/skeleton as long as there is a global loading indicator. Meanwhile the global loading indicator listens to "transitions":
// file: src/components/global-loader.tsx
// from: https://github.com/solidjs/solid-start/blob/2d75d5fedfd11f739b03ca34decf23865868ac09/archived_examples/movies/src/components/GlobalLoader.tsx

import { Show, useTransition } from 'solid-js';
import { useIsRouting } from '@solidjs/router';
import './global-loader.css';

function GlobalLoader() {
const isRouting = useIsRouting();
const [isInTransition] = useTransition();
const isVisible = () => isRouting() || isInTransition();
return (
<Show when={isVisible()}>
<div class="global-loader is-loading">
<div class="global-loader-fill" />
</div>
</Show>
);
}

export { GlobalLoader as default, GlobalLoader };
// file: src/components/global-loader.tsx
// from: https://github.com/solidjs/solid-start/blob/2d75d5fedfd11f739b03ca34decf23865868ac09/archived_examples/movies/src/components/GlobalLoader.tsx

import { Show, useTransition } from 'solid-js';
import { useIsRouting } from '@solidjs/router';
import './global-loader.css';

function GlobalLoader() {
const isRouting = useIsRouting();
const [isInTransition] = useTransition();
const isVisible = () => isRouting() || isInTransition();
return (
<Show when={isVisible()}>
<div class="global-loader is-loading">
<div class="global-loader-fill" />
</div>
</Show>
);
}

export { GlobalLoader as default, GlobalLoader };
Once your loading indicator is driven by (global) transitions, the is little incentive for Suspense fallbacks (other than for first load), and by extension there shouldn't be a need to artificially trigger suspense boundaries.

Did you find this page helpful?