Tutorial
This tutorial takes you through our Todo Example.
Custom chrome formatters
We recommend everyone start by enabling custom chrome formatters for a much better debugging experience.
HomebaseProvider
Welcome back. Let's get started for real.
HomebaseProvider
is a component that wraps your React app and creates a local relational database. This database is then accessible to any child components via React Hooks.
1import React from 'react'
2import { HomebaseProvider, useTransact, useQuery, useEntity } from 'homebase-react'
3
4export const App = () => {
5 return (
6 <HomebaseProvider config={{ lookupHelpers, initialData }}>
7 <Todos/>
8 </HomebaseProvider>
9 )
10}
Lookup Helpers
Unlike other state managers, Homebase does not try to create yet another design pattern for state. Instead, we store state in a way we already know and love: as a relational graph database.
Lookup helpers make relationships and uniqueness constraints something you declare once and then never need to worry about again. Subsequent queries and transactions will take these properties into account so you never have to write a join query.
1const lookupHelpers = {
2 project: {
3 name: {
4 unique: 'identity'
5 }
6 },
7 todo: {
8 // refs are relationships
9 project: {
10 type: 'ref'
11 },
12 owner: {
13 type: 'ref'
14 }
15 }
16}
Initial Data
Hydrate your application with initial data. Here we add an initial user, project, and todo as well as the todoFilter that will filter the initial state to show todos that have been completed.
This data is a transaction that runs on database creation to seed your DB.
1const initialData = [
2 {
3 user: {
4 // negative numbers can be used as temporary ids in a transaction
5 id: -1,
6 name: 'Stella'
7 }
8 }, {
9 project: {
10 id: -3,
11 name: 'To the stars'
12 }
13 }, {
14 todo: {
15 name: 'Fix ship',
16 owner: -1,
17 project: -3,
18 isCompleted: true,
19 createdAt: new Date('2003/11/10')
20 }
21 }, {
22 todoFilter: {
23 // identity is a special attribute for user generated ids
24 // E.g. this is a setting that should be easy to lookup by name
25 identity: 'todoFilters',
26 showCompleted: true,
27 project: 0
28 }
29 }
30]
Config
And now we're ready to go. 馃殌
1const config = {
2 lookupHelpers,
3 initialData
4}
Reading and Writing Data
Use the todoFilters we added earlier to filter the view. With Homebase everything is just data. It's similar to a reducer in React Hooks or Redux, but without the need to write bespoke mutation functions. We also introduce our useEntity
and useTransact
React Hooks here.
useEntity
enables you to grab the todoFilters Entity directly from Homebase by its identity
(a unique developer given name, like a custom id). Entities are the building blocks of the Homebase data model. They are like JSON objects with bonus features: you can traverse arbitrarily deep relationship without actually denormalizing and nesting your data.
useTransact
lets you transact
state from any component. Transactions let you create, update and delete multiple entities simultaneously and atomically. All changes will reactively update any components that depend on the changed data.
1const TodoFilters = () => {
2 const [filters] = useEntity({ identity: 'todoFilters' })
3 const [transact] = useTransact()
4 return (
5 <div>
6 <label htmlFor="show-completed">Show Completed?</label>
7 <input
8 type="checkbox"
9 id="show-completed"
10 checked={filters.get('showCompleted')}
11 onChange={e => transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked }}])}
12 />
13 路
14 <ProjectSelect
15 value={filters.get('project')}
16 onChange={project => transact([{ todoFilter: { id: filters.get('id'), project }}])}
17 />
18 </div>
19 )
20}
Entity and Transact Examples
In the following example todo
is a Homebase database entity being passed in as a prop.
As you probably noticed entities have a convenient function entity.get('attribute')
. It's like jsObj['attribute']
but with a lot of bonus benefits:
- You can chain attributes to traverse your relational graph
todo.get('project', 'owners', 0, 'name') => 'Stella'
- Chaining attributes that return undefined will return null instead of an error
jsObj.nullAttr.childAttr => Error
entity.get('nullAttr', 'childAttr') => null
- Caching is built in. Homebase tracks the attributes used by every component and only triggers re-renders when that specific data changes. This caching is scoped to our hooks, so while we're passing a
todo
entity in the following example it can be better to pass theid
as a prop and thenconst [todo] = useEntity(id)
in the component to create a new caching scope.
1const Todo = ({ todo }) => (
2 <div>
3 <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', paddingTop: 20}}>
4 <TodoCheck todo={todo} />
5 <TodoName todo={todo} />
6 </div>
7 <div>
8 <TodoProject todo={todo} />
9 路
10 <TodoOwner todo={todo} />
11 路
12 <TodoDelete todo={todo} />
13 </div>
14 <small style={{ color: 'grey' }}>
15 {todo.get('createdAt').toLocaleString()}
16 </small>
17 </div>
18)
Notice how id: todo.get('id')
is used in the following example to update an existing todo entity.
1const TodoCheck = ({ todo }) => {
2 const [transact] = useTransact()
3 return (
4 <input
5 type="checkbox"
6 style={{ width: 20, height: 20, cursor: 'pointer' }}
7 checked={!!todo.get('isCompleted')}
8 onChange={e => transact([{
9 todo: {
10 id: todo.get('id'),
11 isCompleted: e.target.checked
12 }
13 }])}
14 />
15 )
16}
17
useTransact
is incredibly convenient. It lets you create, update, and delete any state from any component.
1const TodoName = ({ todo }) => {
2 const [transact] = useTransact()
3 return (
4 <input
5 style={{
6 border: 'none', fontSize: 20, marginTop: -2, cursor: 'pointer',
7 ...todo.get('isCompleted') && { textDecoration: 'line-through '}
8 }}
9 value={todo.get('name')}
10 onChange={e => transact([{ todo: { id: todo.get('id'), name: e.target.value }}])}
11 />
12 )
13}
14
15const TodoProject = ({ todo }) => {
16 const [transact] = useTransact()
17 return (
18 <ProjectSelect
19 value={todo.get('project', 'id') || ''}
20 onChange={projectId => transact([{ todo: { id: todo.get('id'), 'project': projectId || null }}])}
21 />
22 )
23}
Queries
TodoOwner
introduces our first instance of the useQuery
React Hook. Query the relational database directly in your component using Javascript friendly syntax. Queries return an array of unique entities instead of an individual entity like useEntity
.
1const TodoOwner = ({ todo }) => {
2 const [transact] = useTransact()
3 const [users] = useQuery({
4 $find: 'user',
5 $where: { user: { name: '$any' } }
6 })
7...
Here's the full TodoOwner
component:
1const TodoOwner = ({ todo }) => {
2 const [transact] = useTransact()
3 const [users] = useQuery({
4 $find: 'user',
5 $where: { user: { name: '$any' } }
6 })
7 return (
8 <>
9 <label>
10 Owner:
11 </label>
12
13 <select
14 name="users"
15 value={todo.get('owner', 'id') || ''}
16 onChange={e => transact([{ todo: { id: todo.get('id'), owner: Number(e.target.value) || null }}])}
17 >
18 <option value=""></option>
19 {users.map(user => (
20 <option
21 key={user.get('id')}
22 value={user.get('id')}
23 >
24 {user.get('name')}
25 </option>
26 ))}
27 </select>
28 </>
29 ).
Our query API is powered by Datalog, and exposed as JSON similar to a JS SQL driver or MongoDB. Datalog is a subset of Prolog, a logic programming language, but with a few features restricted so it is guaranteed to terminate. This is not a perfect metaphor, but you can think of it as a more powerful version of SQL.
We don't go into Datalog queries here, instead easing you into our JSON query API for simplicity, but you can directly query Homebase
with Datalog as well. If you're interested in writing more sophisticated queries you can pass a datalog string instead of a JSON query as the first argument to useQuery.
E.g.
1const [users] = useQuery(`
2 [:find ?todo
3 :where [?todo :todo/owner ?user]
4 [?user :user/name "Stella"]]
5`)
This will return all todos owned by users named Stella. As you can see joins are implicit by using the ?user
variable in multiple :where
clauses.
We won't get into more detail about Datalog here since it's essentially a programming language. The syntax we use has it's roots in Clojure and Datomic. If you'd like us to priortize documention for these advanced queries please let us know. In the meantime we recommend Learn Datalog Today as a good place to start.
Create Data
It's all just transactions. Yes it's repetitive, but the goal of Homebase it to make data declarative and composable on the client and the server. This means providing a powerful core library and letting you combine the pieces to declare what you want, without needing to say how to do achieve it.
1const NewTodo = () => {
2 const [transact] = useTransact()
3 return (
4 <form onSubmit={e => {
5 e.preventDefault()
6 transact([{
7 todo: {
8 name: e.target.elements['todo-name'].value,
9 createdAt: new Date
10 }
11 }])
12 e.target.reset()
13 }}>
14 <input
15 autoFocus
16 type="text"
17 name="todo-name"
18 placeholder="What needs to be done?"
19 autoComplete="off"
20 required
21 />
22
23 <button type="submit">Create Todo</button>
24 </form>
25 )
26}
Delete Data
The object style transactions support deletion of individual attributes by setting them to null.
1transact({ todo: { id: 123, name: null } })
To delete entire entities we provide a database function retractEntity
. Database functions are atomic functions that run inside of transactions, basically they're reducers. Database functions are invoked by passing an array with the function name first followed by arguments ['retractEntity', 123]
.
1const TodoDelete = ({ todo }) => {
2 const [transact] = useTransact()
3 return (
4 <button onClick={() => transact([['retractEntity', todo.get('id')]])}>
5 Delete
6 </button>
7 )
8}
The Full Example
Here's everything we covered. You can try the app for yourself here.
If you're interested in integrating a backend you can check out our Firebase example here for inspiration or ping us at hi@homebase.io or in our message board to pair on integrating a custom backend.
1import React from 'react'
2import { HomebaseProvider, useTransact, useQuery, useEntity } from 'homebase-react'
3
4export const App = () => {
5 return (
6 <HomebaseProvider config={config}>
7 <Todos />
8 </HomebaseProvider>
9 )
10}
11
12const config = {
13 // Lookup helpers are used to enforce
14 // unique constraints and relationships.
15 lookupHelpers: {
16 project: { name: { unique: 'identity' } },
17 todo: {
18 // refs are relationships
19 project: { type: 'ref' },
20 owner: { type: 'ref' }
21 }
22 },
23 // Initial data let's you conveniently transact some
24 // starting data on DB creation to hydrate your components.
25 initialData: [
26 {
27 user: {
28 // negative numbers can be used as temporary ids in a transaction
29 id: -1,
30 name: 'Stella'
31 }
32 }, {
33 project: {
34 id: -3,
35 name: 'To the stars'
36 }
37 }, {
38 todo: {
39 name: 'Fix ship',
40 owner: -1,
41 project: -3,
42 isCompleted: true,
43 createdAt: new Date('2003/11/10')
44 }
45 }, {
46 todoFilter: {
47 // identity is a special attribute for user generated ids
48 // E.g. this is a setting that should be easy to lookup by name
49 identity: 'todoFilters',
50 showCompleted: true,
51 project: 0
52 }
53 }
54 ]
55}
56
57const Todos = () => {
58 return (
59 <div>
60 <NewTodo />
61 <TodoFilters />
62 <TodoList />
63 </div>
64 )
65}
66
67const NewTodo = () => {
68 const [transact] = useTransact()
69 return (
70 <form onSubmit={e => {
71 e.preventDefault()
72 transact([{
73 todo: {
74 name: e.target.elements['todo-name'].value,
75 createdAt: new Date()
76 }
77 }])
78 e.target.reset()
79 }}>
80 <input
81 autoFocus
82 type="text"
83 name="todo-name"
84 placeholder="What needs to be done?"
85 autoComplete="off"
86 required
87 />
88
89 <button type="submit">Create Todo</button>
90 </form>
91 )
92}
93
94const TodoList = () => {
95 const [filters] = useEntity({ identity: 'todoFilters' })
96 const [todos] = useQuery({
97 $find: 'todo',
98 $where: { todo: { name: '$any' } }
99 })
100 return (
101 <div>
102 {todos.filter(todo => {
103 if (!filters.get('showCompleted') && todo.get('isCompleted')) return false
104 if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) return false
105 if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false
106 return true
107 }).sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1)
108 .map(todo => <Todo key={todo.get('id')} id={todo.get('id')}/>)}
109 </div>
110 )
111}
112
113// PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity
114// this component stays disconnected from the useQuery in the parent TodoList.
115// useEntity creates a separate scope for every Todo so changes to TodoList
116// or sibling Todos don't trigger unnecessary re-renders.
117const Todo = React.memo(({ id }) => {
118 const [todo] = useEntity(id)
119 return (
120 <div>
121 <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', paddingTop: 20}}>
122 <TodoCheck todo={todo} />
123 <TodoName todo={todo} />
124 </div>
125 <div>
126 <TodoProject todo={todo} />
127 路
128 <TodoOwner todo={todo} />
129 路
130 <TodoDelete todo={todo} />
131 </div>
132 <small style={{ color: 'grey' }}>
133 {todo.get('createdAt').toLocaleString()}
134 </small>
135 </div>
136 )
137})
138
139const TodoCheck = ({ todo }) => {
140 const [transact] = useTransact()
141 return (
142 <input
143 type="checkbox"
144 style={{ width: 20, height: 20, cursor: 'pointer' }}
145 checked={!!todo.get('isCompleted')}
146 onChange={e => transact([{ todo: { id: todo.get('id'), isCompleted: e.target.checked } }])}
147 />
148 )
149}
150
151const TodoName = ({ todo }) => {
152 const [transact] = useTransact()
153 return (
154 <input
155 style={{
156 border: 'none', fontSize: 20, marginTop: -2, cursor: 'pointer',
157 ...todo.get('isCompleted') && { textDecoration: 'line-through '}
158 }}
159 defaultValue={todo.get('name')}
160 onChange={e => transact([{ todo: { id: todo.get('id'), name: e.target.value }}])}
161 />
162 )
163}
164
165const TodoProject = ({ todo }) => {
166 const [transact] = useTransact()
167 return (
168 <EntitySelect
169 label="Project"
170 entityType="project"
171 value={todo.get('project', 'id')}
172 onChange={project => transact([{ todo: { id: todo.get('id'), project }}])}
173 />
174 )
175}
176
177const TodoOwner = ({ todo }) => {
178 const [transact] = useTransact()
179 return (
180 <EntitySelect
181 label="Owner"
182 entityType="user"
183 value={todo.get('owner', 'id')}
184 onChange={owner => transact([{ todo: { id: todo.get('id'), owner }}])}
185 />
186 )
187}
188
189const TodoDelete = ({ todo }) => {
190 const [transact] = useTransact()
191 return (
192 <button onClick={() => transact([['retractEntity', todo.get('id')]])}>
193 Delete
194 </button>
195 )
196}
197
198const TodoFilters = () => {
199 const [filters] = useEntity({ identity: 'todoFilters' })
200 const [transact] = useTransact()
201 return (
202 <div>
203 <label>Show Completed?
204 <input
205 type="checkbox"
206 checked={filters.get('showCompleted')}
207 onChange={e => transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked }}])}
208 />
209 </label>
210 路
211 <EntitySelect
212 label="Project"
213 entityType="project"
214 value={filters.get('project')}
215 onChange={project => transact([{ todoFilter: { id: filters.get('id'), project }}])}
216 />
217 路
218 <EntitySelect
219 label="Owner"
220 entityType="user"
221 value={filters.get('owner')}
222 onChange={owner => transact([{ todoFilter: { id: filters.get('id'), owner }}])}
223 />
224 </div>
225 )
226}
227
228const EntitySelect = React.memo(({ label, entityType, value, onChange }) => {
229 const [entities] = useQuery({
230 $find: entityType,
231 $where: { [entityType]: { name: '$any' } }
232 })
233 return (
234 <label>{label}:
235 <select
236 name={entityType}
237 value={value || ''}
238 onChange={e => onChange && onChange(Number(e.target.value) || null)}
239 >
240 <option key="-" value=""></option>
241 {entities.map(entity => (
242 <option key={entity.get('id')} value={entity.get('id')}>
243 {entity.get('name')}
244 </option>
245 ))}
246 </select>
247 </label>
248 )
249})
Thanks!
Thanks for trying us out! We're excited to see what you build and would love to hear any feedback.