Documentation
Loading State
Show skeleton rows while data loads with isLoading.
Show skeleton loaders
Set isLoading while fetching. With no rows, the table fills with skeleton rows. Clear rows when you want a full-table reload.
React TSX
Copy
const [rows, setRows] = useState([]);const [isLoading, setIsLoading] = useState(true);useEffect(() => {fetchRows().then((data) => {setRows(data);setIsLoading(false);});}, []);<SimpleTable columns={columns} rows={rows} isLoading={isLoading} />
Angular
Copy
rows = [];isLoading = true;async ngOnInit() {this.rows = await fetchRows();this.isLoading = false;}<simple-table[columns]="columns"[rows]="rows"[isLoading]="isLoading"></simple-table>
Vue SFC
Copy
<script setup>import { onMounted, ref } from "vue";const rows = ref([]);const isLoading = ref(true);onMounted(async () => {rows.value = await fetchRows();isLoading.value = false;});</script><template><SimpleTable :columns="columns" :rows="rows" :is-loading="isLoading" /></template>
Svelte
Copy
<script>let rows = $state([]);let isLoading = $state(true);$effect(async () => {rows = await fetchRows();isLoading = false;});</script><SimpleTable {columns} {rows} isLoading={isLoading} />
Solid TSX
Copy
const [rows, setRows] = createSignal([]);const [isLoading, setIsLoading] = createSignal(true);onMount(async () => {setRows(await fetchRows());setIsLoading(false);});<SimpleTable columns={columns} rows={rows()} isLoading={isLoading()} />
TypeScript
Copy
const table = new SimpleTableVanilla(container, {columns,rows: [],isLoading: true,});fetchRows().then((rows) => {table.update({ rows, isLoading: false });});
Loading more rows
If rows are already loaded, isLoading keeps them visible and appends skeleton rows below — useful with Pagination and Infinite Scroll.
Example
Scroll to load more and watch skeletons append under existing rows. Use Code or StackBlitz for the full example.
0 rows loaded
React TSX
Copy
1import { useState, useEffect, useCallback } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme } from "@simple-table/react";4import { loadingStateConfig, type LoadingStateEmployee } from "./loading-state.demo-data";5import "@simple-table/react/styles.css";67const LoadingStateDemo = ({8 height = "400px",9 theme10}: {11 height?: string | number;12 theme?: Theme;13}) => {14 const [isLoading, setIsLoading] = useState(true);15 const [data, setData] = useState<LoadingStateEmployee[]>([]);1617 const loadData = useCallback(() => {18 setIsLoading(true);19 setData([]);20 const timer = setTimeout(() => {21 setData([...loadingStateConfig.rows]);22 setIsLoading(false);23 }, 2000);24 return timer;25 }, []);2627 useEffect(() => {28 const timer = loadData();29 return () => clearTimeout(timer);30 }, [loadData]);3132 return (33 <div>34 <div style={{ marginBottom: 12 }}>35 <button36 onClick={() => loadData()}37 disabled={isLoading}38 style={{39 padding: "6px 16px",40 cursor: isLoading ? "not-allowed" : "pointer"41 }}42 >43 {isLoading ? "Loading…" : "Reload Data"}44 </button>45 </div>46 <SimpleTable47 columns={loadingStateConfig.headers}48 getRowId={({ row }) => row.id}49 rows={data}50 isLoading={isLoading}51 height={height}52 theme={theme}53 />54 </div>55 );56};5758export default LoadingStateDemo;
Angularloading-state-demo.component.ts
Copy
1import { Component, Input, OnInit, OnDestroy } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";3import { loadingStateConfig } from "./loading-state.demo-data";4import "@simple-table/angular/styles.css";5import type { LoadingStateEmployee } from "./loading-state.demo-data";67@Component({8 selector: "loading-state-demo",9 standalone: true,10 imports: [SimpleTableComponent],11 template: `12 <div>13 <div style="margin-bottom: 12px">14 <button15 (click)="loadData()"16 [disabled]="isLoading"17 [style.padding]="'6px 16px'"18 [style.cursor]="isLoading ? 'not-allowed' : 'pointer'"19 >20 {{ isLoading ? 'Loading\u2026' : 'Reload Data' }}21 </button>22 </div>23 <simple-table24 [getRowId]="getRowId"25 [rows]="data"26 [columns]="headers"27 [height]="height"28 [theme]="theme"29 [isLoading]="isLoading"30 ></simple-table>31 </div>32 `,33})34export class LoadingStateDemoComponent implements OnInit, OnDestroy {35 @Input() height: string | number = "400px";36 @Input() theme?: Theme;3738 readonly headers: AngularColumnDef<LoadingStateEmployee>[] = loadingStateConfig.headers;39 data: LoadingStateEmployee[] = [];40 isLoading = true;41 private timer: ReturnType<typeof setTimeout> | null = null;4243 ngOnInit(): void {44 this.loadData();45 }4647 ngOnDestroy(): void {48 if (this.timer) clearTimeout(this.timer);49 }5051 loadData(): void {52 this.isLoading = true;53 this.data = [];54 if (this.timer) clearTimeout(this.timer);55 this.timer = setTimeout(() => {56 this.data = loadingStateConfig.rows ;57 this.isLoading = false;58 }, 2000);59 }60}616263// loading-state.demo-data.ts64// Self-contained demo table setup for this example.65import type { AngularColumnDef, ValueFormatterProps } from "@simple-table/angular";6667export interface LoadingStateEmployee {68 id: number;69 name: string;70 age: number;71 department: string;72 salary: number;73 status: string;74}7576export const loadingStateData: LoadingStateEmployee[] = [77 { id: 1, name: "Dr. Elena Vasquez", age: 42, department: "AI Research", salary: 145000, status: "Active" },78 { id: 2, name: "Kai Tanaka", age: 29, department: "UX Design", salary: 95000, status: "Active" },79 { id: 3, name: "Amara Okafor", age: 35, department: "DevOps", salary: 125000, status: "On Leave" },80 { id: 4, name: "Santiago Rodriguez", age: 27, department: "Marketing", salary: 82000, status: "Active" },81 { id: 5, name: "Priya Chakraborty", age: 33, department: "Engineering", salary: 118000, status: "Active" },82 { id: 6, name: "Magnus Eriksson", age: 38, department: "Product", salary: 110000, status: "Inactive" },83 { id: 7, name: "Zara Al-Rashid", age: 31, department: "Sales", salary: 98000, status: "Active" },84 { id: 8, name: "Luca Rossi", age: 26, department: "Marketing", salary: 75000, status: "Active" },85];8687export const loadingStateHeaders: AngularColumnDef<LoadingStateEmployee, any>[] = [88 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },89 { accessor: "age", label: "Age", width: 80, type: "number" },90 { accessor: "department", label: "Department", width: 150 },91 {92 accessor: "salary",93 label: "Salary",94 width: 120,95 type: "number",96 align: "right",97 valueFormatter: ({ value }: ValueFormatterProps<LoadingStateEmployee, number>) => `$${value.toLocaleString()}`,98 },99 { accessor: "status", label: "Status", width: 120 },100];101102export const loadingStateConfig = {103 headers: loadingStateHeaders,104 rows: loadingStateData,105};106
Vue SFC
Copy
1<script setup lang="ts">2import { ref, onMounted } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, GetRowIdParams } from "@simple-table/vue";5import { loadingStateConfig } from "./loading-state.demo-data";6import type { LoadingStateEmployee } from "./loading-state.demo-data";7import "@simple-table/vue/styles.css";89const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {10 height: "400px",11});1213const isLoading = ref(true);14const data = ref<LoadingStateEmployee[]>([]);1516const getRowId = ({ row }: GetRowIdParams<LoadingStateEmployee>) => row.id;1718function loadData() {19 isLoading.value = true;20 data.value = [];21 setTimeout(() => {22 data.value = [...loadingStateConfig.rows];23 isLoading.value = false;24 }, 2000);25}2627onMounted(() => {28 loadData();29});30</script>3132<template>33 <div>34 <div style="margin-bottom: 12px">35 <button36 @click="loadData"37 :disabled="isLoading"38 :style="{ padding: '6px 16px', cursor: isLoading ? 'not-allowed' : 'pointer' }"39 >40 {{ isLoading ? 'Loading\u2026' : 'Reload Data' }}41 </button>42 </div>43 <SimpleTable44 :columns="loadingStateConfig.headers"45 :get-row-id="getRowId"46 :rows="data"47 :is-loading="isLoading"48 :height="props.height"49 :theme="props.theme"50 />51 </div>52</template>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, GetRowIdParams } from "@simple-table/svelte";4 import { onMount } from "svelte";5 import { loadingStateConfig } from "./loading-state.demo-data";6 import type { LoadingStateEmployee } from "./loading-state.demo-data";7 import "@simple-table/svelte/styles.css";89 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1011 let isLoading = $state(true);12 let data = $state<LoadingStateEmployee[]>([]);1314 const getRowId = ({ row }: GetRowIdParams<LoadingStateEmployee>) => row.id;1516 function loadData() {17 isLoading = true;18 data = [];19 setTimeout(() => {20 data = [...loadingStateConfig.rows];21 isLoading = false;22 }, 2000);23 }2425 onMount(() => {26 loadData();27 });28</script>2930<div>31 <div style="margin-bottom: 12px">32 <button33 onclick={loadData}34 disabled={isLoading}35 style="padding: 6px 16px; cursor: {isLoading ? 'not-allowed' : 'pointer'}"36 >37 {isLoading ? "Loading\u2026" : "Reload Data"}38 </button>39 </div>40 <SimpleTable41 columns={loadingStateConfig.headers}42 rows={data}43 getRowId={getRowId}44 {isLoading}45 {height}46 {theme}47 />48</div>
Solid TSX
Copy
1import { createSignal, onMount, onCleanup } from "solid-js";2import { SimpleTable } from "@simple-table/solid";3import type { Theme } from "@simple-table/solid";4import { loadingStateConfig, type LoadingStateEmployee } from "./loading-state.demo-data";5import "@simple-table/solid/styles.css";67export default function LoadingStateDemo(props: { height?: string | number; theme?: Theme }) {8 const [isLoading, setIsLoading] = createSignal(true);9 const [data, setData] = createSignal<LoadingStateEmployee[]>([]);10 let timer: ReturnType<typeof setTimeout> | null = null;1112 const loadData = () => {13 setIsLoading(true);14 setData([]);15 if (timer) clearTimeout(timer);16 timer = setTimeout(() => {17 setData([...loadingStateConfig.rows]);18 setIsLoading(false);19 }, 2000);20 };2122 onMount(() => loadData());23 onCleanup(() => {24 if (timer) clearTimeout(timer);25 });2627 return (28 <div>29 <div style={{ "margin-bottom": "12px" }}>30 <button31 onClick={loadData}32 disabled={isLoading()}33 style={{ padding: "6px 16px", cursor: isLoading() ? "not-allowed" : "pointer" }}34 >35 {isLoading() ? "Loading\u2026" : "Reload Data"}36 </button>37 </div>38 <SimpleTable39 columns={loadingStateConfig.headers}40 getRowId={({ row }) => row.id}41 rows={data()}42 isLoading={isLoading()}43 height={props.height ?? "400px"}44 theme={props.theme}45 />46 </div>47 );48}
TypeScriptLoadingStateDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { LoadingStateEmployee } from "./loading-state.demo-data";3import type { Theme, Row, GetRowIdParams } from "simple-table-core";4import { loadingStateConfig } from "./loading-state.demo-data";5import "simple-table-core/styles.css";678const getRowId = ({ row }: GetRowIdParams<LoadingStateEmployee>) => row.id;9export function renderLoadingStateDemo(10 container: HTMLElement,11 options?: { height?: string | number; theme?: Theme }12): { mount: () => void; destroy: () => void } {13 const wrapper = document.createElement("div");1415 const controls = document.createElement("div");16 controls.style.marginBottom = "12px";1718 const reloadBtn = document.createElement("button");19 reloadBtn.textContent = "Loading\u2026";20 reloadBtn.disabled = true;21 Object.assign(reloadBtn.style, { padding: "6px 16px", cursor: "not-allowed" });22 controls.appendChild(reloadBtn);2324 const tableContainer = document.createElement("div");25 wrapper.appendChild(controls);26 wrapper.appendChild(tableContainer);27 container.appendChild(wrapper);2829 let timer: ReturnType<typeof setTimeout> | null = null;3031 const table = new SimpleTableVanilla(tableContainer, {32 getRowId,33 columns: loadingStateConfig.headers,34 rows: [] ,35 height: options?.height ?? "400px",36 theme: options?.theme,37 isLoading: true,38 });3940 function loadData() {41 reloadBtn.textContent = "Loading\u2026";42 reloadBtn.disabled = true;43 reloadBtn.style.cursor = "not-allowed";44 table.update({ rows: [] , isLoading: true });4546 if (timer) clearTimeout(timer);47 timer = setTimeout(() => {48 table.update({ rows: loadingStateConfig.rows , isLoading: false });49 reloadBtn.textContent = "Reload Data";50 reloadBtn.disabled = false;51 reloadBtn.style.cursor = "pointer";52 }, 2000);53 }5455 reloadBtn.addEventListener("click", loadData);56 loadData();5758 return {59 mount: () => table.mount(),60 destroy: () => {61 if (timer) clearTimeout(timer);62 table.destroy();63 },64 };65}666768// loading-state.demo-data.ts69// Self-contained demo table setup for this example.70import type { ColumnDef } from "simple-table-core";7172export interface LoadingStateEmployee {73 id: number;74 name: string;75 age: number;76 department: string;77 salary: number;78 status: string;79}8081export const loadingStateData: LoadingStateEmployee[] = [82 { id: 1, name: "Dr. Elena Vasquez", age: 42, department: "AI Research", salary: 145000, status: "Active" },83 { id: 2, name: "Kai Tanaka", age: 29, department: "UX Design", salary: 95000, status: "Active" },84 { id: 3, name: "Amara Okafor", age: 35, department: "DevOps", salary: 125000, status: "On Leave" },85 { id: 4, name: "Santiago Rodriguez", age: 27, department: "Marketing", salary: 82000, status: "Active" },86 { id: 5, name: "Priya Chakraborty", age: 33, department: "Engineering", salary: 118000, status: "Active" },87 { id: 6, name: "Magnus Eriksson", age: 38, department: "Product", salary: 110000, status: "Inactive" },88 { id: 7, name: "Zara Al-Rashid", age: 31, department: "Sales", salary: 98000, status: "Active" },89 { id: 8, name: "Luca Rossi", age: 26, department: "Marketing", salary: 75000, status: "Active" },90];9192export const loadingStateHeaders: ColumnDef<LoadingStateEmployee>[] = [93 { accessor: "name", label: "Name", width: "1fr", minWidth: 120 },94 { accessor: "age", label: "Age", width: 80, type: "number" },95 { accessor: "department", label: "Department", width: 150 },96 {97 accessor: "salary",98 label: "Salary",99 width: 120,100 type: "number",101 align: "right",102 valueFormatter: ({ value }) => `$${Number(value).toLocaleString()}`,103 },104 { accessor: "status", label: "Status", width: 120 },105];106107export const loadingStateConfig = {108 headers: loadingStateHeaders,109 rows: loadingStateData,110};111
Props
Loading State Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
isLoadingboolean | Optional | When true with no rows, shows a full page of skeleton loaders. When true with existing rows, keeps that data visible and appends skeleton rows below. Clear rows for a full-table reload. |