S
SolidJS•4d ago
cRambo

SolidTable infinite loop with `createAsync` query, createMemo but not createSignal + createEffect?

I'm writing a SolidStart project, using SolidTable to display data from a backend DB. I'm using SolidStart query to fetch data and SolidStart actions to insert data. I'm running into an issue where if I supply the createAsync accessor of the query directly as the data parameter for the table - it hangs / hits out of memory errors when I insert a row using the action. I'm not 100% sure what causes the issue but it seems like it might be an infinite re-render of the table. If I keep everything the same but change the table data to be an empty array then it works no problems - so not an issue with the DB/query/action by themselves, it's only when its used as data for the table. I've tried some alternative approaches - createMemo of the createAsync accessor does not work - but strangely if I use createSignal and then update it with the value of the createAsync query in createEffect then it does work? Why createSignal/createEffect here work when createAsync does not? Does createAsync not have stable references? Please help me work this out - I'd really like to not use createEffect to make this work if I can avoid it. Thanks!
const getTests = query(async () => {
"use server";
const db = new Kysely<DB>({ dialect });
return await db.selectFrom("tests").selectAll().execute();
}, "getTests");

const addTest = action(async (formData: FormData) => {
"use server";
const db = new Kysely<DB>({ dialect });
const values: InsertableTest = {
// ...
};
await db.insertInto("tests").values(values).execute();
});

export const route = {
preload() {
getTests();
},
} satisfies RouteDefinition;

export default function TestView() {
const addTestAction = useAction(addTest);
const submission = useSubmission(addTest);

// 1. The `createAsync` accessor for the query DOES NOT work!
const data = createAsync<InsertableTest[]>(async () => await getTests());

// 2. Memoization of the `createAsync` query DOES NOT work!
const tableMemo = createMemo(() => data() ?? []);

// 3. Signal updated via `createEffect` DOES work?
const [tableData, setTableData] = createSignal<InsertableTest[]>([]);
createEffect(() => {
setTableData(data() ?? []);
});

return (
<div>
<DataTable columns={columns} data={data()} />
<Button
disabled={submission.pending}
onClick={() => {
const formData = new FormData();
// formData.append(...);
addTestAction(formData);
}}
>
Insert Test
</Button>
</div>
);
}

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[] | undefined;
}

export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
const table = createSolidTable({
get data() {
return props.data ?? [];
},
get columns() {
return props.columns;
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
table.setPageSize(20);
// ...
const getTests = query(async () => {
"use server";
const db = new Kysely<DB>({ dialect });
return await db.selectFrom("tests").selectAll().execute();
}, "getTests");

const addTest = action(async (formData: FormData) => {
"use server";
const db = new Kysely<DB>({ dialect });
const values: InsertableTest = {
// ...
};
await db.insertInto("tests").values(values).execute();
});

export const route = {
preload() {
getTests();
},
} satisfies RouteDefinition;

export default function TestView() {
const addTestAction = useAction(addTest);
const submission = useSubmission(addTest);

// 1. The `createAsync` accessor for the query DOES NOT work!
const data = createAsync<InsertableTest[]>(async () => await getTests());

// 2. Memoization of the `createAsync` query DOES NOT work!
const tableMemo = createMemo(() => data() ?? []);

// 3. Signal updated via `createEffect` DOES work?
const [tableData, setTableData] = createSignal<InsertableTest[]>([]);
createEffect(() => {
setTableData(data() ?? []);
});

return (
<div>
<DataTable columns={columns} data={data()} />
<Button
disabled={submission.pending}
onClick={() => {
const formData = new FormData();
// formData.append(...);
addTestAction(formData);
}}
>
Insert Test
</Button>
</div>
);
}

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[] | undefined;
}

export function DataTable<TData, TValue>(props: DataTableProps<TData, TValue>) {
const table = createSolidTable({
get data() {
return props.data ?? [];
},
get columns() {
return props.columns;
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
table.setPageSize(20);
// ...
3 Replies
Madaxen86
Madaxen86•4d ago
import { createAsync, query } from "@solidjs/router";
import { Suspense } from "solid-js";

const getTests = query(async () => {
"use server"
await new Promise((res) => setTimeout(res, 3000));
return ["test"];
}, "getTest");

const addTest = action(async () => reload({ revalidate: getTests.key }),"addTest"); // you can use a response helper in actions (reload, redirect, json) to pin point which query you want to revalidate through the action.


export default function TestView() {
const data = createAsync(() => getTests(), // no need for async and await here, no need to set type for createAsync as it will infer the type from the server function aka query
{ initialValue: [] } // you can set an initial value to get rid of the undefined, data will be () => string[]`
);

return (
<div>
<h1>test page</h1>
<Suspense>
{/* I like to wrap parts of the app with async data in suspense so the static stuff can be streamed to the client */}
<h3>accessed</h3>
<pre>{JSON.stringify(data())}</pre>
</Suspense>
</div>
);
}
import { createAsync, query } from "@solidjs/router";
import { Suspense } from "solid-js";

const getTests = query(async () => {
"use server"
await new Promise((res) => setTimeout(res, 3000));
return ["test"];
}, "getTest");

const addTest = action(async () => reload({ revalidate: getTests.key }),"addTest"); // you can use a response helper in actions (reload, redirect, json) to pin point which query you want to revalidate through the action.


export default function TestView() {
const data = createAsync(() => getTests(), // no need for async and await here, no need to set type for createAsync as it will infer the type from the server function aka query
{ initialValue: [] } // you can set an initial value to get rid of the undefined, data will be () => string[]`
);

return (
<div>
<h1>test page</h1>
<Suspense>
{/* I like to wrap parts of the app with async data in suspense so the static stuff can be streamed to the client */}
<h3>accessed</h3>
<pre>{JSON.stringify(data())}</pre>
</Suspense>
</div>
);
}
You may have a look at this code. Simplifies a lot. If you call the async accessor inside the createEffect the Suspense boundary in a higher up component will be triggered. I would generally avoid that. If you need to make computations on the client you can do:
const [searchString, setSearchString] = createSignal("abc");
const data = createAsync(() => getTests().then(
(d) => d.filter((item) => item.includes(searchString()))
),
{ initialValue: [] },
);
const [searchString, setSearchString] = createSignal("abc");
const data = createAsync(() => getTests().then(
(d) => d.filter((item) => item.includes(searchString()))
),
{ initialValue: [] },
);
Not sure what causes the infinite loop on the Datatable component.
cRambo
cRamboOP•4d ago
Thanks! lots of good trips in your response 🙂
Madaxen86
Madaxen86•3d ago
Oh and what I'd always recommend with Tanstack table is to seperate data fetching to a higher component because with the createSolidTableyou are accessing the data accessor outside of the JSX so the Suspense inside this component does not pick this up and you'll fallback to higher up Suspenseboundary.
export default function Page() {
const data = createAsync(() => getData());
return (
<>
<h1>Table</h1>
<Suspense>
<DataTable data={data()} />}
</Suspense>
</>
);
}

// also not that with dynamic data we need to pass the data to the createSolidTable primitves with a getter as in their examples
function DataTable(props:{data:TData}){
const table = createSolidTable({
get data() {
return props.data
}
})
//...
}
export default function Page() {
const data = createAsync(() => getData());
return (
<>
<h1>Table</h1>
<Suspense>
<DataTable data={data()} />}
</Suspense>
</>
);
}

// also not that with dynamic data we need to pass the data to the createSolidTable primitves with a getter as in their examples
function DataTable(props:{data:TData}){
const table = createSolidTable({
get data() {
return props.data
}
})
//...
}

Did you find this page helpful?