Using createAsync in SolidJS: Handling Data Dependencies

Currently, I'm thinking of two approaches: Unified Loader Pattern:
const loader = query(async (id: string) => {
"use server";
const criterion = await criterionRepo.find(id);
const programs = await programRepo.findAll();
const variants = await variantRepo.findByProgram(criterion.program_id);
return { criterion, programs, variants };
}, "admin-criterion");
const loader = query(async (id: string) => {
"use server";
const criterion = await criterionRepo.find(id);
const programs = await programRepo.findAll();
const variants = await variantRepo.findByProgram(criterion.program_id);
return { criterion, programs, variants };
}, "admin-criterion");
Separate Async Queries Pattern (commented out):
const criterion = createAsync(() => getCriterion(params.id));
const programs = createAsync(() => getPrograms());
const variants = createAsync(() => getVariantsByProgram(programId()));
const criterion = createAsync(() => getCriterion(params.id));
const programs = createAsync(() => getPrograms());
const variants = createAsync(() => getVariantsByProgram(programId()));
My key questions are: Data Loading Strategy Is it better to use a unified loader that fetches all related data in one query (similar to Remix's loader pattern), or should we use separate createAsync calls for each data dependency? Dynamic Data Updates How should we handle dynamic updates when a program is selected? Currently, the form has this select handler:
<Select
options={data()!.programs}
valueKey="id"
labelKey="name"
select={(program) => program.id === data()!.criterion.program_id}
onChange={(e) => setProgramId(e.currentTarget.value)}
/>
<Select
options={data()!.programs}
valueKey="id"
labelKey="name"
select={(program) => program.id === data()!.criterion.program_id}
onChange={(e) => setProgramId(e.currentTarget.value)}
/>
But changing the program doesn't update the variants because they're loaded once through the unified loader. Dependency Management The variants depend on the selected programId. The current loader handles this initial dependency well, but doesn't account for dynamic updates. Potential solutions: - Keep the loader for initial data but add a separate action/query for variant updates - Return to separate createAsync calls with proper dependency tracking - Implement a hybrid approach What's the most idiomatic way to handle this in SolidJS, considering both initial load and dynamic updates?
3 Replies
binajmen
binajmenOP2w ago
This is what I have after several iteration:
const loader = query(async (criterion_id: string) => {
"use server";
const criterion = await criterionRepo.find(criterion_id);
const programs = await programRepo.findAll();
const variants = await variantRepo.findByProgram(criterion.program_id);
return { criterion, programs, variants };
}, "admin-criterion");

const findVariantsByProgram = action(async (program_id: string) => {
"use server";
const variants = await variantRepo.findByProgram(program_id);
return json(variants, { revalidate: "nothing" });
}, "find-variants-by-program");

export default function AdminCriteriaAggregate() {
const params = useParams();
const data = createAsync(() => loader(params.id));

const findVariants = useAction(findVariantsByProgram);
const findVariantsSubmission = useSubmission(findVariantsByProgram);

const [programId, setProgramId] = createSignal<string | undefined>(
data()?.criterion.program_id,
);

return (
<Show when={data()}>
<form method="post" action={update} class="flex flex-col gap-md">
...
<Field
id="program_id"
label="Programm"
>
{(field) => (
<Select
{...field}
options={data()!.programs}
valueKey="id"
labelKey="name"
select={(program) => program.id === data()!.criterion.program_id}
onChange={(e) => findVariants(e.currentTarget.value)}
/>
)}
</Field>
<Field
id="variant_id"
label="Programmprofil"
>
{(field) => (
<Select
{...field}
options={findVariantsSubmission.result || data()!.variants}
valueKey="id"
labelKey="name"
select={(variant) => variant.id === data()!.criterion.variant_id}
/>
)}
</Field>
const loader = query(async (criterion_id: string) => {
"use server";
const criterion = await criterionRepo.find(criterion_id);
const programs = await programRepo.findAll();
const variants = await variantRepo.findByProgram(criterion.program_id);
return { criterion, programs, variants };
}, "admin-criterion");

const findVariantsByProgram = action(async (program_id: string) => {
"use server";
const variants = await variantRepo.findByProgram(program_id);
return json(variants, { revalidate: "nothing" });
}, "find-variants-by-program");

export default function AdminCriteriaAggregate() {
const params = useParams();
const data = createAsync(() => loader(params.id));

const findVariants = useAction(findVariantsByProgram);
const findVariantsSubmission = useSubmission(findVariantsByProgram);

const [programId, setProgramId] = createSignal<string | undefined>(
data()?.criterion.program_id,
);

return (
<Show when={data()}>
<form method="post" action={update} class="flex flex-col gap-md">
...
<Field
id="program_id"
label="Programm"
>
{(field) => (
<Select
{...field}
options={data()!.programs}
valueKey="id"
labelKey="name"
select={(program) => program.id === data()!.criterion.program_id}
onChange={(e) => findVariants(e.currentTarget.value)}
/>
)}
</Field>
<Field
id="variant_id"
label="Programmprofil"
>
{(field) => (
<Select
{...field}
options={findVariantsSubmission.result || data()!.variants}
valueKey="id"
labelKey="name"
select={(variant) => variant.id === data()!.criterion.variant_id}
/>
)}
</Field>
The main point is:
options={findVariantsSubmission.result || data()!.variants}
options={findVariantsSubmission.result || data()!.variants}
And using an action to retrieve the data via useSubmission, but this seems convoluted..
Madaxen86
Madaxen862w ago
The problem with a unified loader is that you are creating unnecessary waterfalls. In your example the programm query does not depend on anything but the query only gets called after criterion has resolved and then it blocks the variant query. With seperate queries each with createAsync those queries run all in parallel. => faster TTFB. Another benefit is that createAsync tracks dependencies. So setting programmId causes getCriterion to refetch with the new ID. But then you have to be aware that the variants query might run with programmId when it's still undefined so you might want to guard against that like
//function that gets the data
const getData = async (id: string) => {
"use server";
return id;
};

const queryData = query(async (id?: string) => {
if (!id) return; //guard if id is undefined return on the client no need to send a request to the server
return await getData(id);
}, "queryData");

export default function Component() {
const [id, setId] = createSignal<string>();
const data = createAsync(() => queryData(id()));
return <h1>Some JSX</h1>
}
//function that gets the data
const getData = async (id: string) => {
"use server";
return id;
};

const queryData = query(async (id?: string) => {
if (!id) return; //guard if id is undefined return on the client no need to send a request to the server
return await getData(id);
}, "queryData");

export default function Component() {
const [id, setId] = createSignal<string>();
const data = createAsync(() => queryData(id()));
return <h1>Some JSX</h1>
}
For preloading solid-start and the router provide the preload option where you'd just call the function
binajmen
binajmenOP6d ago
is this a pattern to return early from an action like that? this could indeed solve my problem.. i'll try this tomorrow and see how the network tab behave !

Did you find this page helpful?