Skip to content

James O'Claire

My personal site

Menu
  • Home
  • Projects
    • Projects Overview
    • App Goblin: App Scraper
    • Android Apps
    • Ads Data Dash
  • Contact
Menu

Easiest way to get Tanstack Table v8 working with Svelte 5!

Posted on April 9, 2025 by James O'Claire

The problem: Tanstack v8 does not support Svelte 5.

This turned out to be much easier than I thought by using the data-table component already built by huntabyte! I was previously aware of this for another project where I used shadcn-svelte with Svelte 5 and tanstack but in my skeleton project as the tanstack v8 installation instructions say you should ‘build your own adapter’ to get it working with Svelte 5.

Copy Components from shadcn-svelte

My project is using Skeleton, so I wasn’t sure how well this would work, it turned out I was worried about nothing, this was super easy, even when not using shadcn-svelte

First head to shadcn-svelte ui/data-table and copy these into your own project. I put mine into lib/components/data-table.

Next, follow the steps outlined by huntabyte shadcn-svelte for datatables, I found this was helpful for me to make sure I got the various parts of tanstack table setup correctly as well as using createSvelteTable from Huntabyte’s project.

Optional goodies: Pagination, Sorting. For me these were not optional but now that I realized the resource that was the shadcn-svelte implemtation I was able to adapt them from the Svelte 4 data-table implementation here.

Example Table Code

Here’s my final Svelte 5 + Skeleton v3 + Tanstack v8 data-table component and here’s an example of a table I made with those parts with Pagination, sorting and CSV export:

<script lang="ts" generics="TData, TValue">
	import {
		type ColumnDef,
		type PaginationState,
		type SortingState,
		getCoreRowModel,
		getPaginationRowModel,
		getSortedRowModel
	} from '@tanstack/table-core';
	import { renderComponent } from '$lib/components/data-table/index.js';
	import { mkConfig, generateCsv, download } from 'export-to-csv';

	import ColumnSortButton from '$lib/components/data-table/ColumnSortButton.svelte';

	import Pagination from '$lib/components/data-table/Pagination.svelte';

	import { createSvelteTable, FlexRender } from '$lib/components/data-table/index.js';
	import type { KeywordScore } from '../types';

	const columns: ColumnDef<KeywordScore>[] = [
		{
			accessorKey: 'keyword_text',
			header: ({ column }) =>
				renderComponent(ColumnSortButton, {
					columnTitle: 'Keyword',
					sortDirection: column.getIsSorted(),
					onclick: () => {
						const currentSort = column.getIsSorted();
						if (currentSort === false) {
							column.toggleSorting(false); // Set to ascending
						} else if (currentSort === 'asc') {
							column.toggleSorting(true); // Set to descending
						} else {
							column.clearSorting(); // Clear sorting (back to unsorted)
						}
					}
				})
		},
		{
			accessorKey: 'competitiveness_score',
			header: ({ column }) =>
				renderComponent(ColumnSortButton, {
					columnTitle: 'Competitiveness Score',
					sortDirection: column.getIsSorted(),
					onclick: () => {
						const currentSort = column.getIsSorted();
						if (currentSort === false) {
							column.toggleSorting(false); // Set to ascending
						} else if (currentSort === 'asc') {
							column.toggleSorting(true); // Set to descending
						} else {
							column.clearSorting(); // Clear sorting (back to unsorted)
						}
					}
				})
		},
		{
			accessorKey: 'd30_best_rank',
			header: ({ column }) =>
				renderComponent(ColumnSortButton, {
					columnTitle: '30 Day Best Rank',
					sortDirection: column.getIsSorted(),
					onclick: () => {
						const currentSort = column.getIsSorted();
						if (currentSort === false) {
							column.toggleSorting(false); // Set to ascending
						} else if (currentSort === 'asc') {
							column.toggleSorting(true); // Set to descending
						} else {
							column.clearSorting(); // Clear sorting (back to unsorted)
						}
					}
				})
		}
	];

	type DataTableProps<KeywordScore, TValue> = {
		// columns: ColumnDef<TData, TValue>[];
		data: KeywordScore[];
	};

	let { data }: DataTableProps<KeywordScore, TValue> = $props();
	let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
	let sorting = $state<SortingState>([]);

	const table = createSvelteTable({
		get data() {
			return data;
		},
		columns,
		state: {
			get pagination() {
				return pagination;
			},
			get sorting() {
				return sorting;
			}
		},

		getSortedRowModel: getSortedRowModel(),
		onSortingChange: (updater) => {
			if (typeof updater === 'function') {
				sorting = updater(sorting);
			} else {
				sorting = updater;
			}
		},

		onPaginationChange: (updater) => {
			if (typeof updater === 'function') {
				pagination = updater(pagination);
			} else {
				pagination = updater;
			}
		},
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel()
	});

	const csvConfig = mkConfig({
		fieldSeparator: ',',
		filename: 'appgoblin_data', // export file name (without .csv)
		decimalSeparator: '.',
		useKeysAsHeaders: true
	});

	const exportDataCSV = (rows: Row<_>[]) => {
		const rowData = rows.map((row) => row.original);
		const csv = generateCsv(csvConfig)(rowData);
		download(csvConfig)(csv);
	};
</script>

<div class="table-container space-y-4">
	<div class="overflow-x-auto pl-0">
		<table class="md:table table-hover md:table-compact table-auto w-full text-xs">
			<thead>
				{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
					<tr>
						{#each headerGroup.headers as header (header.id)}
							<th>
								{#if !header.isPlaceholder}
									<FlexRender
										content={header.column.columnDef.header}
										context={header.getContext()}
									/>
								{/if}
							</th>
						{/each}
					</tr>
				{/each}
			</thead>
			<tbody>
				{#each table.getRowModel().rows as row (row.id)}
					<tr>
						<td class="table-cell-fit">
							<a href="/keywords/en/{row.original.keyword_text}"> {row.original.keyword_text}</a>
						</td>
						<td class="table-cell-fit">
							{row.original.competitiveness_score}
							<p class="inline text-xs text-secondary-400-600">{row.original.app_count} apps</p>
						</td>
						<td class="table-cell-fit">
							{row.original.d30_best_rank}
						</td>
					</tr>
				{:else}
					<tr>
						<td colspan={columns.length} class="h-24 text-center">No results.</td>
					</tr>
				{/each}
			</tbody>
		</table>
		<footer class="flex justify-between">
			<div class="flex items-center justify-end space-x-2 py-4">
				<Pagination tableModel={table} />
				<button
					type="button"
					class="btn btn-sm preset-outlined-primary-100-900 p-0"
					onclick={() => exportDataCSV(table.getFilteredRowModel().rows)}
				>
					Download CSV
				</button>
			</div>
		</footer>
	</div>
</div>




Categories

  • Development
  • Mobile Marketing and Advertising
  • Uncategorized

Recent Posts

  • How to self host your own S3 in 2025
  • Apple: The Silent Advertising Monopoly
  • Watching the Watchers: What to track when tracking app trackers?
  • Free Mobile App ASO Tools: Fastest Growing Apps & Keyword Research added to AppGoblin
  • 2025 How to Sniff Android HTTPS Traffic with Waydroid & mitm-proxy

Recent Comments

    Archives

    • May 2025
    • April 2025
    • March 2025
    • February 2025
    • January 2025
    • December 2024
    • November 2024
    • October 2024
    • March 2024
    • February 2024
    • January 2024
    • November 2023
    • October 2023
    • September 2023
    • October 2022
    • April 2016
    • March 2016
    • February 2016

    Meta

    • Log in
    • Entries feed
    • Comments feed
    • WordPress.org
    © 2025 James O'Claire | Powered by Minimalist Blog WordPress Theme