umma.dev

TanStack: Start

What is TanStack?

TanStack is a collection of type-safe, framework-agnostic libraries for frontend development.It includes libraries for data fetching (Query), tables, forms, routing, and other core application concerns. While commonly used with React, most TanStack libraries also support Vue, Solid and Svelte.

Why Should you Use TanStack?

TanStack addresses several technical challenges:

  • Framework independence - Core logic remains consistent across React, Vue, Solid, and Svelte
  • TypeScript-first design - Built with TypeScript from the ground up
  • Modularity - Individual libraries solve specific problems without unnecessary coupling
  • Performance optimisation - Careful attention to render cycles and memory usage
  • Consistent APIs - Similar patterns across libraries reduce cognitive overhead

What is TanStack Start?

TanStack Start is a CLI tool that automates project scaffolding with TanStack libraries. It handles dependency installation, configuration setup, and folder structure creation based on selected options.

Getting Started

Basic usage:

# Install CLI
npm install -g @tanstack/cli

# Initialise project
tanstack init my-project

# Install dependencies and start
cd my-project
npm install
npm run dev

During initialisation, TanStack Start prompts for:

  • Framework selection
  • TanStack libraries to include
  • TypeScript configuration
  • Styling solution
  • Testing framework

Generated project structure:

my-project/
├── src/
│   ├── components/
│   ├── hooks/
│   ├── pages/
│   ├── App.tsx
│   └── main.tsx
├── .eslintrc.js
├── package.json
├── tsconfig.json
└── vite.config.ts

Example Use Cases

Data Table with Filtering

import {
  createColumnHelper,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { useQuery } from "@tanstack/react-query";

function DataTable() {
  // Fetch data
  const { data = [] } = useQuery({
    queryKey: ["table-data"],
    queryFn: () => fetch("/api/data").then((res) => res.json()),
  });

  // Define columns
  const columnHelper = createColumnHelper<DataItem>();
  const columns = [
    columnHelper.accessor("id", { header: "ID" }),
    columnHelper.accessor("name", { header: "Name" }),
    columnHelper.accessor("status", {
      header: "Status",
      cell: ({ getValue }) => {
        const status = getValue();
        return <span className={`status-${status}`}>{status}</span>;
      },
    }),
  ];

  // Initialise table
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  // Render table
  return (
    <table>
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th key={header.id}>
                {header.column.columnDef.header as React.ReactNode}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map((row) => (
          <tr key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <td key={cell.id}>
                {cell.column.columnDef.cell
                  ? cell.column.columnDef.cell(cell.getContext())
                  : (cell.getValue() as React.ReactNode)}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Form with Validation

import { useForm } from "@tanstack/react-form";
import { z } from "zod";

// Define validation schema
const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

function LoginForm() {
  const form = useForm({
    defaultValues: {
      email: "",
      password: "",
    },
    onSubmit: async (values) => {
      // Submit logic
      console.log(values);
    },
    validatorAdapter: {
      validate: async (values) => {
        try {
          schema.parse(values);
          return { success: true };
        } catch (error) {
          return { success: false, error };
        }
      },
    },
  });

  return (
    <form.Provider>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
      >
        <form.Field name="email">
          {(field) => (
            <div>
              <label>Email</label>
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
              />
              {field.state.meta.errors && (
                <p className="error">{field.state.meta.errors}</p>
              )}
            </div>
          )}
        </form.Field>

        <form.Field name="password">
          {(field) => (
            <div>
              <label>Password</label>
              <input
                type="password"
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
              />
              {field.state.meta.errors && (
                <p className="error">{field.state.meta.errors}</p>
              )}
            </div>
          )}
        </form.Field>

        <button type="submit">Login</button>
      </form>
    </form.Provider>
  );
}

Router Integration

import { Route, Router, createBrowserHistory } from "@tanstack/react-router";

// Define routes
const rootRoute = new Route({
  component: RootLayout,
});

const indexRoute = new Route({
  getParentRoute: () => rootRoute,
  path: "/",
  component: HomePage,
});

const usersRoute = new Route({
  getParentRoute: () => rootRoute,
  path: "users",
  component: UsersPage,
});

const userRoute = new Route({
  getParentRoute: () => usersRoute,
  path: "$userId",
  component: UserDetailsPage,
});

// Create router
const routeTree = rootRoute.addChildren([
  indexRoute,
  usersRoute.addChildren([userRoute]),
]);

const router = new Router({
  routeTree,
  history: createBrowserHistory(),
});

Further Reading