Releases: tinyplex/tinybase
v7.3.0
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:
- The useTablesState hook for reading and writing all Tables
- The useTableState hook for reading and writing a single Table
- The useRowState hook for reading and writing a single Row
- The useCellState hook for reading and writing a single Cell
- The useValuesState hook for reading and writing all Values
- The useValueState hook for reading and writing a single Value
- The useParamValuesState hook for reading and writing all query ParamValues
- The useParamValueState hook for reading and writing a single query ParamValue
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:
- The Countries demo now uses the useCellState hook for the star/unstar toggle functionality.
- The Todo App demo uses the useCellState hook to simplify todo completion toggling, and the useValueState hook for managing the selected type filter.
- The Car Analysis demo uses the useParamValueState hook to manage query parameters for dimensions, measures, and aggregates. Much more elegant!
Check out these demos to see the state hooks in action.
v7.2.0
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:
- getParamValues method
- getParamValue method
- setParamValues method
- setParamValue method
- addParamValuesListener method
- addParamValueListener method
It also includes the following new React hooks:
- useParamValues hook
- useParamValue hook
- useSetParamValuesCallback hook
- useSetParamValueCallback hook
- useParamValuesListener hook
- useParamValueListener hook
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
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:
- Zod via the createZodSchematizer function
- TypeBox via the createTypeBoxSchematizer function
- Valibot via the createValibotSchematizer function
- ArkType via the createArkTypeSchematizer function
- Yup via the createYupSchematizer function
- Effect Schema via the createEffectSchematizer function
For more information, see the Using Schematizers guide.
v7.0.1
v7.0.0
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'));
// -> trueTo 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:
nullis an explicit value. A Cell set to null exists in the Store.undefinedmeans 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
NULLwill have the valuenull
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:
-
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.
-
Update your schemas: Add allowNull: true to Cell definitions that should permit
nullvalues:
store.setTablesSchema({
pets: {
species: {type: 'string'},
color: {type: 'string', allowNull: true},
age: {type: 'number', allowNull: true},
},
});- 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
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
This release updates dependencies.
v6.7.2
This release updates dependencies.
v6.7.1
This release updates dependencies, and migrates the test suite from Jest to Vitest.
v6.7.0
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!