Working with Tables

Explore the full Tables API

Tables are the backbone of Trigger and offer unique experiences compared to other React state management libraries.

To create tables in your store, you first declare the table type, where each property represents a column/attribute/feature in your table:

import { extract, CreateTable, type Store, type Table } from '@datahook/trigger';

type TaskOwner = {
    ownerID: number | null;
    firstName: string;
    lastName: string;
    lastUpdated: Date;
    isActive: boolean;
    preferences: null | {
        nickname: string;
        email: string;
    };
    assignedTaskIDs: number[];
}

In Trigger, the following basic types are supported:

  • string
  • number
  • Date
  • boolean
  • null

Your tables are made up of rows. Each row can have columns (or attributes) that use any of the basic types listed above. In addition to these basic types, your tables can also use:

  • Arrays (lists of the basic types)
  • Objects (collections that can hold any of the basic types)

All types can be unioned with null

The following types are not currently supported in a table. To use these types in Trigger, you useSingles instead.

  • undefined
  • Functions
  • Maps
  • Sets
  • WeakMaps
  • WeakSets
  • Classes

When you insert a row into a table, trigger will automatically handle assigning the _id value to your rows. If you need to store nested objects, you have two options:

  1. Redefine the nested nature of your objects as flat tables with properties (columns) you can use to relate to one another
  2. Use Trigger's Singles data type, which allows any kind of data to be stored

Trigger will use our table types to improve the developer experience as you interact with your store.

You create your store with the types you have declared. When creating the table, you pass an array of column names for your table that match the keys in your type:

export const store = CreateStore({
    taskOwners: CreateTable<TaskOwner>(['ownerID', 'firstName', 'lastName']),
});

The CreateTable function provides the functionality Trigger uses to manage your table. A couple of points worth noting when calling CreateTable:

  • You must pass all properties for your table
  • You can pass an object where each property is a column name and the value is an empty array, or you can prepopulate the arrays with values
  • If you prepopulate the arrays with values, all arrays must contain the same number of elements or Trigger will throw an error

Declaring our table types and store types are important because they provide an improved developer experience across your application. To use your table in your application, you simply import the store value and use the namespaced table:

component.tsx
import { store } from './store';

function MyComponent() {
    const rows = store.activeTasks.use();

    return (
        <div>
            There are {rows.length} active tasks in the table
        </div>
    )
}

Behind the scenes, Trigger will assign a unique, sequential _id column to each of your tables. This property is accessed as _id and provides many conveniences for the internal functions of Trigger, and can be used in your application to uniquely identify each row. For instance, the _id property is a great candidate for the key property when creating lists in React.

use() and useById()

Note: methods starting with use will cause your component to rerender. No other method will cause your component to rerender.

With tables, the use and useById methods are how you can update your component when changes occur to the table.

The use method will return an array of rows matching you where predicate and your notify list. If your where predicate is set to null, it means you would like to receive all rows in the table. If your where predicate is a user-defined function, the function will receive each row in the table individually and the user must return a boolean to determine if the row should be retrieved. The notify list for use can accept insert, update, and delete.

  • insert: any time a new row is inserted in the table, re-render this component
  • update: any time a row is updated in the table, re-render the component
  • delete: any time a row is deleted in the table, re-render the component

Examples of use():

// retrieve all rows from the table, and re-render it every time there is a change to the table
const rows = store.activeTasks.use();
// retrieve all rows from the table, and re-render it every time a row is inserted or deleted
const rows = store.activeTasks.use(null, ['onInsert', 'onDelete']);
// retrieve only rows from the table where the "value" property is greater than 10,
// and re-render it every time a row is inserted.
const rows = store.activeTasks.use(v => v.value > 10, ['onInsert']);
Note: Trigger will compare the current values in your component to the new values to determine if a re-render is required

The useById method will return a single row or undefined for any row that matches the _id. In practice, use and useById are often used in tandem, where are parent component will pass the _id to child components who then subscribe to that row's changes. The notify list for use can accept onUpdate, and onDelete.

  • onUpdate: if the row is updated, re-render the component
  • onDelete: if the row is deleted from the table, re-render the component

Examples of useById():

// retrieve the row from the table whose "_id" property is 10
const row = store.activeTasks.useById(10);
// retrieve the row from the table whose "_id" property is 10,
// and only re-render the component when the row is deleted
const row = store.activeTasks.useById(10, ['onDelete']);

find(), findOne() and findById()

The find(), findOne() and findById() methods are similar to use() and useById() except they will not cause your component to rerender. These methods are handy for when you want to retrieve rows from your store, but you don't want to cause a re-render.

The find() method will return an array of rows matching your where predicate. Your where predicate can be an object whose properties are the columns in your table and values are those that you want to match, or a user-defined function that receives each entry in the table individually and the user must return a boolean to determine if the row should be retrieved. If no entries are found, the function will return an empty array.

Examples of find():

// retrieve all rows in the table where the "firstName" property is set to "Ada"
const rows = store.activeTasks.find({ firstName: 'Ada' });
// retrive all rows in the table where the "ownerID" is greater than 10.
const rows = store.activeTasks.find(v => v.ownerID > 10);

The findOne() method is the same as the find() method, except it will only return the first row that is found, or undefined if no row matches your where predicate. Omitting the where predicate will return the first row in the table.

Examples of findOne():

// retrieve the first row in the table where "firstName" property is set to "Ada"
const row = store.activeTasks.findOne({ firstName: 'Ada' });
// retrieve the first row in the table where the "ownerID" is greater than 10.
const row = store.activeTasks.findOne(v => v.ownerID > 10);

The findById() method will return the row matching the provided _id predicate. Your where predicate can be an (_id).

Examples of findById():

// retrieve the row from the table whose "_id" property is 10
const row = store.activeTasks.findById(10);
Built with in Halifax, Nova Scotia by JW