8000 GitHub - homebaseio/homebase-react at v0.1.1
[go: up one dir, main page]

Skip to content

homebaseio/homebase-react

Repository files navigation

Homebase React

< 8000 /div>

CI CD NPM Version Bundle Size License

The graph database for delightful React state management

Installation

# NPM
npm install homebase-react --save

# Yarn
yarn add homebase-react

Purpose

Data should be a first class citizen on the client. We deserve the ergonomics and power of relational databases not just on the server, but in our React applications.

Homebase React lets you plug a relational database into your React application with just 3 lines of code. In fact, it's the same database that powers Roam Research and many other ClojureScript applications, but with an API that's familiar to React and JS developers.

  • Replace Redux with something simpler and more declarative
  • Stop spending time wiring up custom datatypes, reducers, caches and other bespoke state mumbo jumbo
  • Gain all the benefits of relational data like a single source of truth
  • Traverse your data graph like it's one big JSON object
  • Query your database with a convenient JSON query syntax
  • Query your database with Clojure style Datalog if you need more power
  • It's just data

The goal of Homebase React is to be immediately more intuitive than any denormalized JSON store and over time we will eliminate all the tedious aspects of manipulating data on clients. Homebase React makes it easier to work with complex data on client by making our applications local-first.

Examples

Live Demos

You can see our hosted live demos here

Code Examples

You can clone and run our React code examples here.

API Overview

HomebaseProvider

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

import { HomebaseProvider, useEntity, useTransact, useQuery } from 'homebase-react'

const config = {
  // Schema is not a type system like in most DBs. 
  // That is something we're considering, but for now it is 
  // mainly applied at query time to simplify relational queries.
  // The only schema currently supported is:
  // `type: 'ref'` which is a relationship and
  // `unique: 'identity` which enforces a uniqueness constraint 
  // and let's you lookup entities by their unique attributes.
  schema: {
    todo: {
      project: { type: 'ref' },
      name: { unique: 'identity' }
    }
  },
  
  // Initial data is what it sounds like.
  // It's a transaction that runs on component mount.
  // Use it to hydrate your app.
  initialData: [
    { project: { id: -1, name: 'Do it', owner: -2 } },
    { todo: { project: -1, name: 'Make it' } },
    { user: { id: -2, name: 'Arpegius' } }
  ]
}

const RootComponent = () => (
  <HomebaseProvider config={config}>
    <App/>
  </HomebaseProvider>
)

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 relationship without actually denormalizing and nesting your data.

// You can get an entity by its id and get attributes off of it.
const [todo] = useEntity(2)
todo.get('id') // => 2
todo.get('name') // => 'Make it'

// Entities with unique attributes can also be retrieved by those attributes.
const [sameTodo] = useEntity({ todo: { name: 'Make it' } })
sameTodo.get('id') // => 2

// And most importantly you can traverse arbitrarily deep relationships.
sameTodo.get('project', 'owner', '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.

const transact = useTransact()

// A transaction is an array of nested objects and or arrays.
// Leaving the id blank will create a new entity.
transact([{ todo: { name: 'New Todo', project: 1 } }])

// Setting the id to a negative number is a temp id which 
// allows multiple entities to be related to each other on creation.
transact([
  { project: { id: -123, name: 'New Project' } },
  { todo: { project: -123, name: 'New Todo' } },
])

// Update an entity by including its id.
// NOTE: that only the included attributes will be updated.
transact([{ project: { id: 1, name: 'Changed Project Title' } }])

// To remove an attribute you have to explicitly set it to null.
transact([{ project: { id: 1, name: null } }])

// To delete an entire entity use retractEntity and its id
transact([['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 Mongo DB. Datalog is similar to SQL and is incredibly powerful. However only a subset of features are currently available in JSON.

We are very interested in what features the community wants, and will prioritize based on feedback. In the meantime you can further filter results with JS filter() and sort().

// Finds all todos with a name
const [todos] = useQuery({
  $find: 'todo',
  $where: { todo: { name: '$any' } }
})

// Returns an array of todo entities
todos
.sort((todo1, todo2) => todo1.get('name') > todo2.get('name') ? 1 : -1)
.map(todo => todo.get('name'))

Performance

Homebase React tracks the attributes consumed in each component via the entity.get function and scopes those attributes to their respective useEntity or useQuery hook. Re-renders are only triggered when an attribute changes.

The default caching reduces unnecessary re-renders and virtual DOM thrashing a lot. That said, it is still possible to trigger more re-renders than you might want.

One top level useQuery + prop drilling the entities it returns will cause all children to re-render on any change to the parent or their siblings.

To fix this we recommend passing ids to children, not whole entities. Instead get the entity in the child with useEntity(id). This creates a new scope for each child so they are not affected by changes in the state of the parent or sibling components.

const TodoList = () => {
  const [todos] = useQuery({
    $find: 'todo',
    $where: { todo: { name: '$any' } }
  })
  return (todos.map(t => <Todo key={t.get('id')} id={t.get('id')} />))
}

// Good
const Todo = React.memo(({ id }) => {
  const [todo] = useEntity(id)
  // ...
})

// Bad
const Todo = React.memo(({ todo }) => {
  // ...
})

Docs

https://www.notion.so/Homebase-Alpha-Docs-0f0e22f3adcd4e9d87a13440ab0c7a0b

Roadmap

  1. Improve performance
  2. Document integration with backends
  3. Swap Datascript out for Datahike
    1. Immutability
    2. History / Change Tracking
  4. Persist to IndexDB
  5. Local-first conflict resolution for offline caching and sync between multiple devices

Development

yarn install
yarn dev

Open http://localhost:3000

Test

yarn install
yarn test

Contributing

Welcome and thank you! Writing docs, patches and features are all incredibly helpful and appreciated.

We only ask that you provide test coverage for code changes, and conform to our commit guidelines.

Authors

0