Suggest an edit

API

HomebaseProvider

The HomebaseProvider wraps your React app and makes a relational database accessible to all of your components. Configure it with schema and initialData.

1import { HomebaseProvider, useEntity, useTransact, useQuery } from 'homebase-react'
2
3const config = {
4  // Schema is not a type system,
5  // it's a way to simplify relational queries at query time.
6  // The schema currently supported is:
7  // `type: 'ref'` which is a relationship and
8  // `unique: 'identity` which enforces a uniqueness constraint 
9  // and lets you lookup entities by their unique attributes.
10  schema: {
11    todo: {
12      project: { type: 'ref', cardinality: 'one' },
13      name: { unique: 'identity' }
14    }
15  },
16  
17  // Initial data is what it sounds like.
18  // It's a transaction that runs on component mount.
19  // Use it to hydrate your app.
20  initialData: [
21    { project: { id: -1, name: 'Do it', user: -2 } },
22    { todo: { project: -1, name: 'Make it' } },
23    { user: { id: -2, name: 'Arpegius' } }
24  ]
25
26  // Or relationships can be specified implicitly with nested JSON
27  initialData: [
28    { 
29      todo: { 
30        name: 'Make it',
31        project: { 
32          name: 'Do it', 
33          user: { 
34            name: 'Arpegius' 
35          } 
36        } 
37      } 
38    }
39  ]
40}
41
42const RootComponent = () => (
43  <HomebaseProvider config={config}>
44    <App/>
45  </HomebaseProvider>
46)

useEntity and entity.get

Entities are the building blocks of the Homebase data model. They are like JSON objects with bonus features. In particular you can traverse arbitrarily deep relationships without actually denormalizing and nesting your data.

1// You can get an entity by its id and get attributes off of it.
2const [todo] = useEntity(2)
3todo.get('id') // => 2
4todo.get('name') // => 'Make it'
5
6// Entities with unique attributes can also be retrieved by those attributes.
7const [sameTodo] = useEntity({ todo: { name: 'Make it' } })
8sameTodo.get('id') // => 2
9
10// And most importantly you can traverse arbitrarily deep relationships.
11sameTodo.get('project', 'user', 'name') // => 'Arpegius'

useTransact

Transactions let you create, update and delete multiple entities simultaneously. All changes will reactively update any components that depend on the changed data.

1const transact = useTransact()
2
3// A transaction is an array of nested objects and or arrays.
4// Leaving the id blank will create a new entity.
5transact([{ todo: { name: 'New Todo', project: 1 } }])
6
7// Setting the id to a negative number is a temp id which 
8// allows multiple entities to be related to each other on creation.
9transact([
10  { project: { id: -123, name: 'New Project' } },
11  { todo: { project: -123, name: 'New Todo' } },
12])
13
14// Update an entity by including its id.
15// NOTE: that only the included attributes will be updated.
16transact([{ project: { id: 1, name: 'Changed Project Title' } }])
17
18// To remove an attribute you have to explicitly set it to null.
19transact([{ project: { id: 1, name: null } }])
20
21// To delete an entire entity use retractEntity and its id
22transact([['retractEntity', 1]])

useQuery

Use queries to return an array of entities that meet a given criteria. Our query API is powered by Datalog, but exposed as JSON similar to a JS SQL driver or MongoDB. Datalog is similar to SQL and is incredibly powerful. However, only a subset of features are currently available in JSON.

We will prioritize features based on community feedback so please open an issue if there's something you need. In the meantime you can further filter results with JS filter() and sort().

1// Finds all todos with a name
2const [todos] = useQuery({
3  $find: 'todo',
4  $where: { todo: { name: '$any' } }
5})
6
7// Returns an array of todo entities
8todos
9.sort((todo1, todo2) => todo1.get('name') > todo2.get('name') ? 1 : -1)
10.map(todo => todo.get('name'))

useClient

This hook returns the current database client with some helpful functions for syncing data with a backend.

  • client.dbToString() serializes the whole db including the schema to a string
  • client.dbFromString('a serialized db string') replaces the current db
  • client.dbToDatoms() returns an array of all the facts aka datoms saved in the db
    • datoms are the smallest unit of data in the database, like a key value pair but better
    • they are arrays of [entityId, attribute, value, transactionId, isAddedBoolean]
  • client.addTransactListener((changedDatoms) => ...) adds a listener function to all transactions
    • use this to save data to your backend
  • client.removeTransactListener() removes the transaction listener
    • please note that only 1 listener can be added per useClient scope
  • client.transactSilently([{item: {name: ...}}]) like transact() only it will not trigger any listeners
    • use this to sync data from your backend into the client

Check out the Firebase example for a demonstration of how you might integrate a backend.

Arrays & Nested JSON

Arrays and arbitrary JSON are partially supported for convenience. However in most cases its better to avoid arrays. Using a query and then sorting by an attribute is simpler and more flexible. This is because arrays add extra overhead to keep track of order.

1const config = {
2  schema: {
3    company: {
4      numbers: { type: 'ref', cardinality: 'many' },
5      projects: { type: 'ref', cardinality: 'many' },
6    }
7  }
8}
9
10transact([
11  { project: { id: -1, name: 'a' } },
12  { 
13    company: {
14      numbers: [1, 2, 3],
15      projects: [
16        { project: { id: -1 } },
17        { project: { name: 'b' } },
18      ]
19    }
20  }
21])
22
23// Index into arrays
24company.get('numbers', 1, 'value') // => 2
25company.get('projects', 0, 'ref', 'name') // => 'a'
26// Get the automatically assigned order
27// Order starts at 1 and increments by 1
28company.get('numbers', 0, 'order') // => 1
29company.get('projects', 0, 'order') // => 1
30company.get('projects', 1, 'order') // => 2
31// Map over individual attributes
32company.get('numbers', 'value') // => [1, 2, 3]
33company.get('projects', 'ref', 'name') // => ['a', 'b']

The entity.get API is flexible and supports indexing into arrays as well as automatically mapping over individual attributes.

Array items are automatically assigned an order and either a value or a ref depending on if item in the array is an entity or not. To reorder an array item change its order.

1transact([
2  { 
3    id: company.get('numbers', 2, 'id'), 
4    order: (company.get('numbers', 0, 'order') 
5          + company.get('numbers', 1, 'order')) / 2
6  }
7])
8
9company.get('numbers', 'value') // => [1 3 2]

If you need to transact complex JSON like arrays of arrays then you're better off serializing it to a string first.

1// NOT supported
2transact([{ company: { matrix: [[1, 2, 3], [4, 5, 6]] } }])
3
4// Better
5transact([{ company: { matrix: JSON.stringify([[1, 2, 3], [4, 5, 6]]) } }])
6JSON.parse(company.get('matrix'))

For more information check out the JSON Derived Relationships blog post