Skip to content

Releases: tinyplex/tinybase

v7.3.0

15 Dec 17:02

Choose a tag to compare

Introducing State Hooks

This release introduces a new family of convenience hooks that follow React's useState pattern, making it even easier to read and write TinyBase data in your React components.

Each state hook returns a tuple containing both the current value and a setter function, eliminating the need to use separate getter and setter hooks.

State hooks combine the functionality of getter hooks (like the useRow hook) and setter callback hooks (like the useSetRowCallback hook) into a single, convenient API that feels just like React's useState:

import {createStore} from 'tinybase';
import {useCellState} from 'tinybase/ui-react';

const store = createStore().setRow('pets', 'fido', {
  species: 'dog',
  color: 'brown',
});

const PetEditor = () => {
  const [color, setColor] = useCellState('pets', 'fido', 'color', store);

  return (
    <div>
      <div>Color: {color}</div>
      <button onClick={() => setColor('black')}>Change Color</button>
    </div>
  );
};

Available State Hooks

This release includes eight new state hooks covering the most common data access patterns:

These hopefully mean less boilerplate, are particularly useful when building forms, editors, or any interactive UI that needs bidirectional data binding.

Demo Updates

We've updated a few of the demos to showcase these new state hooks:

Check out these demos to see the state hooks in action.

v7.2.0

12 Dec 20:53

Choose a tag to compare

Introducing Parameterized Queries!

This release introduces parameterized queries to TinyQL - finally!

These allow you to define queries using named 'params' that you can then easily update to change the query's results - without redefining the whole query each time.

Let's take a look with a simple example:

import {createStore, createQueries} from 'tinybase';

const store = createStore().setTable('pets', {
  fido: {age: 2, species: 'dog'},
  felix: {age: 1, species: 'cat'},
  cujo: {age: 3, species: 'dog'},
});

const queries = createQueries(store).setQueryDefinition(
  'petsBySpecies',
  'pets',
  ({select, where, param}) => {
    select('age');
    where('species', param('species'));
  },
  {species: 'dog'}, // Initial param value
);

console.log(queries.getResultTable('petsBySpecies'));
// -> {fido: {age: 2}, cujo: {age: 3}}

// Update the 'species' param to 'cat' to change the results:
queries.setParamValue('petsBySpecies', 'species', 'cat');
console.log(queries.getResultTable('petsBySpecies'));
// -> {felix: {age: 1}}

You can of course have multiple params in a query definition, and use them in any part of the query definition that you would like. Listeners also work as expected - if you are listening to a query's results, and you change a param that affects those results, your listener will be called accordingly.

This is TinyBase so you should not be too surprised... but params themselves are reactive! You can get and listen to their values with the getParamValue method and addParamValueListener method, for example.

For React users, we also shipped a bunch of new hooks that cover params in exactly the way you would expect, including the useSetParamValueCallback hook and the useSetParamValuesCallback hook, which let you easily update param values from, say, an event handler in your application.

We know this has been a long-awaited feature, so we hope you enjoy it! See the TinyQL guide for more details, and please let us know how it goes!

Demos

We have updated the Movie Database demo to use parameterized queries, and as a result is more efficient and easier to (we think) understand. See the yearGenreMovies, directedMovies, and appearedMovies queries to see params in action.

We have also updated the Car Analysis demo to use just one single parameterized query for the whole app!

Full API additions

This release includes the following new Queries interface methods:

It also includes the following new React hooks:

Check out the API docs for each. They should seem very familiar.

Please check out this new release and let us know what you think!

v7.1.0

11 Dec 07:30

Choose a tag to compare

This release introduces Schematizers, a new system for converting schemas from popular validation libraries into TinyBase's schema format.

Schematizers provide a bridge between external schema validation libraries (like Zod, TypeBox, and Valibot) and TinyBase's TablesSchema and ValuesSchema formats. Instead of manually writing TinyBase schemas, you can now convert existing schemas at runtime:

import {createStore} from 'tinybase';
import {createZodSchematizer} from 'tinybase/schematizers/schematizer-zod';
import {z} from 'zod';

const schematizer = createZodSchematizer();

const zodSchema = {
  pets: z.object({
    species: z.string(),
    age: z.number(),
    sold: z.boolean().default(false),
  }),
};

const schematizedStore = createStore().setTablesSchema(
  schematizer.toTablesSchema(zodSchema),
);

schematizedStore.setRow('pets', 'fido', {species: 'dog', age: 3});
console.log(schematizedStore.getRow('pets', 'fido'));
// -> {species: 'dog', age: 3, sold: false}

Schematizers perform best-effort conversions, extracting basic type information (string, number, boolean), defaults, and nullable settings from your schemas.

This release includes support for:

For more information, see the Using Schematizers guide.

v7.0.1

05 Dec 18:50

Choose a tag to compare

This makes a small change to the generic typing of the persister StatusListener type so that all specialized Persisters are correctly understood to be subtypes of Persister, fixing #273

v7.0.0

04 Dec 04:00

Choose a tag to compare

This important (and slightly breaking!) release adds support for null as a valid Cell and Value type, alongside string, number, and boolean.

Null Type Support

You can now set Cells and Values to null:

import {createStore} from 'tinybase';

const store = createStore();
store.setCell('pets', 'fido', 'species', 'dog');
store.setCell('pets', 'fido', 'color', null);

console.log(store.getCell('pets', 'fido', 'color'));
// -> null

console.log(store.hasCell('pets', 'fido', 'color'));
// -> true

To allow null values in your schema, use the new allowNull property:

store.setTablesSchema({
  pets: {
    species: {type: 'string'},
    color: {type: 'string', allowNull: true},
  },
});

store.setCell('pets', 'fido', 'color', null);
// Valid because allowNull is true

store.setCell('pets', 'fido', 'species', null);
// Invalid - species does not allow null

store.delSchema();

Important Distinction: null vs undefined

It's crucial to understand the difference between null and undefined in TinyBase:

  • null is an explicit value. A Cell set to null exists in the Store.
  • undefined means the Cell does not exist in the Store.

This means that the hasCell method will return true for a Cell with a null value:

store.setCell('pets', 'fido', 'color', null);
console.log(store.hasCell('pets', 'fido', 'color'));
// -> true

store.delCell('pets', 'fido', 'color');
console.log(store.hasCell('pets', 'fido', 'color'));
// -> false

store.delTables();

Breaking Change: Database Persistence

Important: This release includes a breaking change for applications using database persisters (the Sqlite3Persister, PostgresPersister, or PglitePersister interfaces, for example).

SQL NULL values are now loaded as TinyBase null values. Previously, SQL NULL would result in Cells being absent from the Store. Now, SQL NULL maps directly to TinyBase null, which means:

  • Tables loaded from SQL databases will be dense rather than sparse
  • Every Row will have every Cell Id present in the table schema
  • Cells that were SQL NULL will have the value null

Example of the roundtrip transformation via a SQLite database:

import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';

const sqlite3 = await sqlite3InitModule();
let db = new sqlite3.oo1.DB(':memory:', 'c');

store.setTable('pets', {
  fido: {species: 'dog'},
  felix: {species: 'cat', color: 'black'},
});

const tabularPersister = createSqliteWasmPersister(store, sqlite3, db, {
  mode: 'tabular',
  tables: {save: {pets: 'pets'}, load: {pets: 'pets'}},
});

await tabularPersister.save();
// After saving the the SQL database:
// SQL table: fido (species: 'dog', color: NULL)
//           felix (species: 'cat', color: 'black')

await tabularPersister.load();
// After loading again, the Store now has a dense table with an explicit null:

console.log(store.getRow('pets', 'fido'));
// -> {species: 'dog', color: null}

This is the correct semantic mapping since SQL databases have fixed schemas where every row must account for every column. See the Database Persistence guide for more details.

Migration Guide

If you are using database persisters, you should:

  1. Review your data access patterns: If you were checking hasCell(...) === false to detect missing data, you now need to check getCell(...) === null for null values.

  2. Update your schemas: Add allowNull: true to Cell definitions that should permit null values:

store.setTablesSchema({
  pets: {
    species: {type: 'string'},
    color: {type: 'string', allowNull: true},
    age: {type: 'number', allowNull: true},
  },
});
  1. Consider memory implications: Dense tables consume more memory than sparse tables. If you have large tables with many optional Cells, this could be significant.

v6.7.5

29 Nov 04:33

Choose a tag to compare

This release updates dependencies, including Automerge v2.5.0.

It also adds an agents.md file to make it easier for AI agents to understand the system.

v6.7.3

13 Nov 05:41

Choose a tag to compare

This release updates dependencies.

v6.7.2

25 Oct 02:11

Choose a tag to compare

This release updates dependencies.

v6.7.1

11 Oct 09:01

Choose a tag to compare

This release updates dependencies, and migrates the test suite from Jest to Vitest.

v6.7.0

04 Oct 01:43

Choose a tag to compare

This release includes support for the Origin Private File System (OPFS) in a browser. The createOpfsPersister function is the main entry point, and is available in the existing persister-browser module:

import {createStore} from 'tinybase';
import {createOpfsPersister} from 'tinybase/persisters/persister-browser';

const opfs = await navigator.storage.getDirectory();
const handle = await opfs.getFileHandle('tinybase.json', {create: true});

const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createOpfsPersister(store, handle);

await persister.save();
// Store JSON will be saved to the OPFS file.

await persister.load();
// Store JSON will be loaded from the OPFS file.

await persister.destroy();

That's it! If you've used other TinyBase persisters, this API should be easy and familiar to use.

One caveat: observability in OPFS is not yet standardized in browsers. This means that the auto-load functionality of the persister may not work as expected, although a best effort is made using the experimental FileSystemObserverAPI, so please let us know how that works!