Tarstate is a set of hooks for React (and other libs), that lets you query state trees like a database. It's a fast, disposable way to glue unrelated data sources together.
Note: Tarstate is alpha quality software.
Perf and GC targets systems programming and video games. Work is shared between queries, and aims to be faster than hand-rolled state management at scale. Database adapters are provided for Automerge and generic atomic external stores; Zustand supplies a thin adapter to the external-store contract.
It also generates JSON-serializable schemas, describing your data in terms of relationships, that TS can read as types. It is intended to support schema evolution, i.e. so changing your state tree in the future won't break things.
Tarstate was heavily inspired by wotbrew/relic, after Out of the Tar Pit.
Queries are portable values. The typed authoring helpers preserve exact result rows and parameters through preparation and into the UI:
import {
prepareTypedQuery,
typedFrom,
typedOrderBy,
typedSelect
} from '@tarstate/core/query/authoring';
import { relationLiteral } from '@tarstate/core/schema';
import { schema } from './schema';
const pizzas = relationLiteral(schema, 'pizzas');
const pizza = typedFrom(pizzas, 'pizza');
const pizzaMenuQuery = typedSelect(
typedOrderBy(pizza, aliases => [{
value: aliases.pizza.row.name,
direction: 'asc'
}]),
'menu',
aliases => ({
name: aliases.pizza.row.name,
price: aliases.pizza.row.price
})
);
const queryScope = {
registryFingerprint: 'registry:application-v1',
authorityFingerprint: 'authority:application-v1',
datasetId: 'primary'
} as const;
export const pizzaMenuPlan = await prepareTypedQuery(pizzaMenuQuery, queryScope);Prepared queries are compiled into an Incremental View Maintenance graph. When data changes, Tarstate updates affected operators and reuses shared work.
Schemas are JSON manifests that describe the shape and relationships of data.
They are different from json-schema, and justified by parse not validate.
Schemas can live alongside (or inside) your data, making versioning almost free:
import { sealSchema } from '@tarstate/core/schema';
export const schema = await sealSchema({
"id": "example.pizza-ordering@1",
"body": {
"relations": {
"bases": {
"relationId": "example.base",
"key": ["name"],
"fields": {
"name": { "type": { "kind": "string" } },
"style": { "type": { "kind": "string" } }
}
},
"pizzas": {
"relationId": "example.pizza",
"key": ["name"],
"fields": {
"name": { "type": { "kind": "string" } },
"base": { "type": { "kind": "ref", "target": { "relationId": "example.base" } } },
"price": { "type": { "kind": "number" } }
}
},
"toppings": {
"relationId": "example.topping",
"key": ["pizza", "name"],
"fields": {
"pizza": { "type": { "kind": "ref", "target": { "relationId": "example.pizza" } } },
"name": { "type": { "kind": "string" } },
"extra": { "type": { "kind": "boolean" } }
}
}
}
}
});
export const initialState = {
bases: [
{ name: 'thin', style: 'Neapolitan' },
{ name: 'pan', style: 'Detroit' }
],
pizzas: [
{ name: 'margherita', base: ['thin'], price: 18 },
{ name: 'pepperoni', base: ['thin'], price: 21 }
],
toppings: [
{ pizza: ['margherita'], name: 'mozzarella', extra: false },
{ pizza: ['margherita'], name: 'basil', extra: false },
{ pizza: ['pepperoni'], name: 'pepperoni', extra: true }
]
}Tarstate can read existing state management through a prepared query while preserving its inferred row type:
import { TarstateProvider, useQuery } from '@tarstate/react';
import { pizzaMenuPlan } from './queries';
import { database } from './tarstate';
export function App() {
return (
<TarstateProvider database={database}>
<PizzaMenu />
</TarstateProvider>
);
}
function PizzaMenu() {
const menu = useQuery(pizzaMenuPlan);
if (menu.state === 'closed') return null;
return (
<section>
<h2>Pizza menu</h2>
<ul>
{menu.current.rows.map((row, index) => (
<li key={menu.current.resultKeys[index]}>
{row.name} - ${row.price}
</li>
))}
</ul>
</section>
);
}Schemas should be your source-of-truth for types. You can generate TypeScript declarations whenever they change:
import { writeFile } from 'node:fs/promises';
import { generateSchemaOutputs } from '@tarstate/schema-tools';
import { schema } from './schema';
const generated = await generateSchemaOutputs(schema);
if (!generated.success) throw new Error(generated.issues[0]?.code);
await writeFile('src/generated/tarstate/rows.d.ts', generated.value.typescript);This script writes src/generated/tarstate/rows.d.ts. As it's in src, TypeScript and most IDEs will see the automatic types.