TinyBase - A tiny, reactive JavaScript library for structured state and tabular data
A JavaScript library for structured state.
Using plain old JavaScript objects to manage data gets old very quickly. It’s error-prone, tricky to track changes efficiently, and easy to mistakenly incur performance costs.
TinyBase is a smarter way to structure your application state:
- Familiar concepts of tables, rows, and cells, and schematization to model your data domain.
- Flexibly reactive to reconciled updates, so you only spend cycles on the data that changes.
- Indexing, metrics, relationships – and even an undo stack for your app state! – out of the box.
- Easily sync your data to local or remote storage, and use idiomatic bindings to your React UI.
Tiny by name, tiny by nature, TinyBase only costs 2.6kB – 5.5kB when compressed, and has zero dependencies. And of course it’s well tested, fully documented, and open source. Other FAQs?
Set and get tables, rows, and cells.
Creating a Store
requires just a simple call to the createStore
function. Once you have one, you can easily set Table
, Row
, or Cell
values by their Id
. And of course you can easily get the values back out again.
Read more about setting and changing data in The Basics guide.
const store = createStore()
.setTable('pets', {fido: {species: 'dog'}})
.setCell('pets', 'fido', 'color', 'brown');
console.log(store.getRow('pets', 'fido'));
// -> {species: 'dog', color: 'brown'}
Register listeners at any granularity.
The magic starts to happen when you register listeners on a Store
, Table
, Row
, or Cell
. They get called when any part of that object changes. You can also use wildcards – useful when you don’t know the Id
of the objects that might change.
Read more about listeners in the Listening To Stores guide.
const listenerId = store.addTableListener('pets', () =>
console.log('changed'),
);
store.setCell('pets', 'fido', 'sold', false);
// -> 'changed'
store.delListener(listenerId);
Call React hooks to bind to data.
If you’re using React in your application, the optional ui-react
module provides hooks to bind to the data in a Store
.
More magic! The useCell
hook in this example fetches the dog’s color. But it also registers a listener on that cell that will fire and re-render the component whenever the value changes.
Basically you simply describe what data you want in your user interface and TinyBase will take care of the whole lifecycle of updating it for you.
Read more about the using hooks in the Using React Hooks guide.
const App1 = () => {
const color = useCell('pets', 'fido', 'color', store);
return <>Color: {color}</>;
};
const app = document.createElement('div');
ReactDOM.render(<App1 />, app);
console.log(app.innerHTML);
// -> 'Color: brown'
store.setCell('pets', 'fido', 'color', 'walnut');
console.log(app.innerHTML);
// -> 'Color: walnut'
Use components to make reactive apps.
The react module provides simple React components with bindings that make it easy to create a fully reactive user interface based on a Store
.
In this example, the library’s RowView
component just needs a reference to the Store
, the tableId
, and the rowId
in order to render the contents of that row. An optional cellComponent
prop lets you override how you want each Cell
rendered. Again, all the listeners and updates are taken care of for you.
The module also includes a context Provider that sets up default for an entire app to use, reducing the need to drill all your props down into your app’s hierarchy.
Most of the demos showcase the use of these React hooks and components. Take a look at Todo App v1 (the basics) to see these user interface binding patterns in action.
Read more about the ui-react
module in the Building UIs guides.
const MyCellView = (props) => (
<>
{props.cellId}: <CellView {...props} />
<hr />
</>
);
const App2 = () => (
<RowView
store={store}
tableId="pets"
rowId="fido"
cellComponent={MyCellView}
/>
);
ReactDOM.render(<App2 />, app);
console.log(app.innerHTML);
// -> 'species: dog<hr>color: walnut<hr>sold: false<hr>'
store.setCell('pets', 'fido', 'sold', true);
console.log(app.innerHTML);
// -> 'species: dog<hr>color: walnut<hr>sold: true<hr>'
ReactDOM.unmountComponentAtNode(app);
Apply schemas to tables.
By default, a Row
can contain any arbitrary Cell
. But you can add a schema to a Store
to ensure that the values are always what you expect. For example, you can limit their types, and provide defaults. You can also create mutating listeners that can programmatically enforce a schema.
In this example, we set a second Row
without the sold
Cell
in it. The schema ensures it’s present with default of false
.
Read more about schemas in the Using Schemas guide.
store.setSchema({
pets: {
species: {type: 'string'},
color: {type: 'string'},
sold: {type: 'boolean', default: false},
},
});
store.setRow('pets', 'felix', {species: 'cat'});
console.log(store.getRow('pets', 'felix'));
// -> {species: 'cat', sold: false}
store.delSchema();
Persist data to browser, file, or server.
You can easily persist a Store
between browser page reloads or sessions. You can also synchronize it with a web endpoint, or (if you’re using TinyBase in an appropriate environment), load and save it to a file.
Read more about persisters in the Persisting Data guide.
const persister = createSessionPersister(store, 'demo');
await persister.save();
console.log(sessionStorage.getItem('demo'));
// -> '{"pets":{"fido":{"species":"dog","color":"walnut","sold":true},"felix":{"species":"cat","sold":false}}}'
persister.destroy();
sessionStorage.clear();
Define metrics and aggregations.
A Metrics
object makes it easy to keep a running aggregation of Cell
values in each Row
of a Table
. This is useful for counting rows, but also supports averages, ranges of values, or arbitrary aggregations.
In this example, we create a new table of the pet species, and keep a track of which is most expensive. When we add horses to our pet store, the listener detects that the highest price has changed.
Read more about Metrics
in the Using Metrics guide.
store.setTable('species', {
dog: {price: 5},
cat: {price: 4},
worm: {price: 1},
});
const metrics = createMetrics(store);
metrics.setMetricDefinition(
'highestPrice', // metricId
'species', // tableId to aggregate
'max', // aggregation
'price', // cellId to aggregate
);
console.log(metrics.getMetric('highestPrice'));
// -> 5
metrics.addMetricListener('highestPrice', () =>
console.log(metrics.getMetric('highestPrice')),
);
store.setCell('species', 'horse', 'price', 20);
// -> 20
metrics.destroy();
Create indexes for fast lookups.
An Indexes
object makes it easy to look up all the Row
objects that have a certain value in a Cell
.
In this example, we create an index on the species
Cell
values. We can then get the the list of distinct Cell
value present for that index (known as ‘slices’), and the set of Row
objects that match each value.
Indexes
objects are reactive too. So you can set listeners on them just as you do for the data in the underlying Store
.
Read more about Indexes
in the Using Indexes guide.
const indexes = createIndexes(store);
indexes.setIndexDefinition(
'bySpecies', // indexId
'pets', // tableId to index
'species', // cellId to index
);
console.log(indexes.getSliceIds('bySpecies'));
// -> ['dog', 'cat']
console.log(indexes.getSliceRowIds('bySpecies', 'dog'));
// -> ['fido']
indexes.addSliceIdsListener('bySpecies', () =>
console.log(indexes.getSliceIds('bySpecies')),
);
store.setRow('pets', 'lowly', {species: 'worm'});
// -> ['dog', 'cat', 'worm']
indexes.destroy();
Model relationships between tables.
A Relationships
object lets you associate a Row
in a local Table
with the Id
of a Row
in a remote Table
. You can also reference a table to itself to create linked lists.
In this example, the species
Cell
of the pets
Table
is used to create a relationship to the species
Table
, so that we can access the price of a given pet.
Like everything else, you can set listeners on Relationships
too.
Read more about Relationships
in the Using Relationships guide.
const relationships = createRelationships(store);
relationships.setRelationshipDefinition(
'petSpecies', // relationshipId
'pets', // local tableId to link from
'species', // remote tableId to link to
'species', // cellId containing remote key
);
console.log(
store.getCell(
relationships.getRemoteTableId('petSpecies'),
relationships.getRemoteRowId('petSpecies', 'fido'),
'price',
),
);
// -> 5
relationships.destroy();
Set checkpoints for an undo stack.
A Checkpoints
object lets you set checkpoints on a Store
. Move forward and backward through them to create undo and redo functions.
In this example, we set a checkpoint, then sell one of the pets. Later, the pet is brought back to the shop, and we go back to that checkpoint to revert the store to its previous state.
Read more about Checkpoints
in the Using Checkpoints guide.
const checkpoints = createCheckpoints(store);
checkpoints.addCheckpoint('pre-sale');
store.setCell('pets', 'felix', 'sold', true);
console.log(store.getCell('pets', 'felix', 'sold'));
// -> true
checkpoints.goBackward();
console.log(store.getCell('pets', 'felix', 'sold'));
// -> false
Did we say tiny?
If you use the basic store
module alone, you’ll only add a gzipped 2.6kB to your app. You can incrementally add the other modules as you need more functionality, or get it all for 5.5kB. The ui-react
adaptor is just another 2.6kB, and everything is fast.
Life’s easy when you have zero dependencies.
Read more about how TinyBase is structured in the Architecture guide.
.js.gz | .js | debug.js | .d.ts | |
---|---|---|---|---|
store | 2.6kB | 5.8kB | 23.8kB | 86.9kB |
indexes | 1.5kB | 2.9kB | 13.0kB | 27.4kB |
metrics | 1.5kB | 3.0kB | 12.3kB | 26.6kB |
relationships | 1.5kB | 3.1kB | 14.3kB | 39.0kB |
checkpoints | 1.3kB | 2.4kB | 10.0kB | 30.7kB |
persisters | 0.8kB | 1.6kB | 4.9kB | 26.7kB |
common | 0.1kB | 0.1kB | 0.1kB | 3.5kB |
tinybase | 5.5kB | 13.0kB | 53.9kB | 0.3kB |
Well tested and documented.
TinyBase has 100.0% test coverage, including the code throughout the documentation – even on this page! The guides, demos, and API examples are designed to make it as easy as possible to get up and running.
Read more about how TinyBase is tested in the Unit Testing guide.
Total | Tested | Coverage | |
---|---|---|---|
Lines | 933 | 933 | 100.0% |
Statements | 1,018 | 1,018 | 100.0% |
Functions | 366 | 366 | 100.0% |
Branches | 341 | 341 | 100.0% |
Tests | 1,712 | ||
Assertions | 7,959 |
Follow
About
Building TinyBase was an interesting exercise in API design, minification, and documentation. It’s not my day job, but I do intend to maintain it, so please provide feedback. I could not have done this without these great projects and friends, and I hope you enjoy using it!