useFireproof hook for React
Using Fireproof in your React app is as easy as running:
npm install use-fireproof
Then in your app, you can use the top-level useLiveQuery
hook to get access to the database and live query responses. Here's an example to-do list that initializes the database and sets up automatic refresh for query results. It also uses the database.put
function to add new todos. With sync connected, the list of todos will redraw for all users in real-time. Here's the code:
import { useLiveQuery } from 'use-fireproof';
export default TodoList = () => {
const todos = useLiveQuery('date').docs;
const database = useLiveQuery.database;
const [newTodo, setNewTodo] = useState('');
return (
<div>
<input type="text" onChange={(e) => setNewTodo(e.target.value)} />
<button
onClick={() =>
database.put({ text: newTodo, date: Date.now(), completed: false })
}
>
Save
</button>
<ul>
{todos.map((todo) => (
<li key={todo._id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => database.put({ ...todo, completed: !todo.completed })}
/>
{todo.text}
</li>
))}
</ul>
</div>
);
};
This example shows calling useLiveQuery
and database.put
. It may be all you need to get started. You can try out a running version here.
Hooks usage
useLiveQuery
In your components, the database
object and useLiveQuery
hook are returned from the useFireproof
hook. You can use the useLiveQuery
hook to subscribe to query results, and automatically redraw when necessary. When sync is enabled you'll have both parties updating the same database in real-time. Here's an example of a simple shared to-do list. For something like a form you should use Live Document instead. There are two ways to call useLiveQuery
- as a top-level hook, or based on the return value of useFireproof
, which allows you to specifc the database name and replication options. Most apps will start with the top-level useLiveQuery
hook, and then move to the lower-level API when they need more control.
import { useLiveQuery } from 'use-fireproof';
export default TodoList = () => {
const todos = useLiveQuery('date').docs
...
The top-level call (above) will use the default database name, and the default replication options. You can also call useLiveQuery
with a database name and replication options, by instantiating the useFireproof
hook directly. Here's an example that uses the lower-level API:
import { useFireproof } from 'use-fireproof';
export default TodoList = () => {
const { database, useLiveQuery } = useFireproof("my-todo-app")
const todos = useLiveQuery('date').docs
...
This running CodePen example uses the useLiveQuery
to display a list of todos, and the database.put
function to add new todos. With sync connected, the list of todos will redraw for all users in real-time. Here's the code:
useDocument
You can also subscribe directly to database updates, and automatically redraw when necessary. When sync is enabled you'll have both parties updating the same database in real-time. Here's an example of a simple shared text area (in real life you'd probably want to use an operational transform library like Yjs or Automerge for shared text areas, which both work great with Fireproof).
Just like useLiveQuery, you can call useDocument as a top-level hook, or based on the return value of useFireproof. Here's an example that uses the top-level hook:
import { useDocument } from 'use-fireproof';
const CustomerProfile = ({ customerId }) => {
const [doc, setDoc, saveDoc] = useDocument({
_id: `${customerId}-profile`,
name: "",
company: "",
startedAt: Date.now()
});
return (
<div>
<form>
Name:
<input
type="text"
value={doc.name}
onChange={(e) => setDoc({ name: e.target.value })}
/>
Company:
<input
type="text"
value={doc.company}
onChange={(e) => setDoc({ company: e.target.value })}
/>
<button
onClick={(e) => {
e.preventDefault();
saveDoc();
}}
>
Save
</button>
</form>
<p>Started at: {doc.startedAt}</p>
<pre>{JSON.stringify(doc, null, 2)}</pre>
</div>
);
};
The top-level call (above) will use the default database name, and the default replication options. You can also call useLiveQuery
with a database name and replication options, by instantiating the useFireproof
hook directly. Here's an example that uses the lower-level API:
import { useFireproof } from 'use-fireproof';
const CustomerProfile = ({ customerId }) => {
const { database, useDocument } = useFireproof("my-todo-app")
const [doc, setDoc, saveDoc] = useDocument({
_id: `${customerId}-profile`,
name: "",
company: "",
createdAt: new Date()
});
...
Another simple use case for Live Document is a shared form, where multiple users can edit the same document at the same time. For something like a chat room you should use Live Query instead:
import { useDocument } from 'use-fireproof'
function MyComponent() {
const [doc, setDoc, saveDoc] = useDocument({ _id : "my-doc-id" })
return <input
value={doc.text}
onChange={(e) => setDoc({text : e.target.value});}
/><button onClick={saveDoc}>Save</button>
}
Using the useFireproof
hook
The other top level hook, useFireproof
, takes two optional setup function arguments, defineDatabaseFn
and setupDatabaseFn
. See below for examples.
The return value looks like { useLiveQuery, useDocument, database, ready }
where the database
is the Fireproof instance that you can interact with using put
and get
, or via your indexes. The ready
flag turns true after setup completes, you can use this to activate your UI. The useLiveQuery
and useDocument
functions are hooks used to update your app in real-time.
Changes made via remote sync peers, or other members of your cloud replica group will appear automatically if you use these APIs. Makes writing collaborative workgroup software, and multiplayer games super easy.
Raw database subscription
Here is an example that uses direct database APIs instead of document and query hooks. You might see this in more complex applications that want to manage low-level details.
import { useFireproof } from 'use-fireproof';
function MyComponent() {
const { ready, database } = useFireproof();
// set a default empty document
const [doc, setDoc] = useState({});
// run the loader on first mount
useEffect(() => {
const getDataFn = async () => {
setDoc(await database.get('my-doc-id'));
};
getDataFn();
return database.subscribe(getDataFn);
}, [database]);
// a function to change the value of the document
const updateFn = async () => {
await database.put({ _id: 'my-doc-id', hello: 'world', updated_at: new Date() });
};
// render the document with a click handler to update it
return <pre onclick={updateFn}>{JSON.stringify(doc)}</pre>;
}
This should result in a tiny application that updates the document when you click it. In a real application you'd probably query an index to present eg. all of the photos in a gallery.
Setup Functions
defineDatabaseFn
Synchronous function that defines the database, run this before any async calls. You can use it to do stuff like set up Indexes. Here's an example:
const defineIndexes = (database) => {
new Index(database, 'allLists', function (doc, map) {
if (doc.type === 'list') map(doc.type, doc);
});
new Index(database, 'todosByList', function (doc, map) {
if (doc.type === 'todo' && doc.listId) {
map([doc.listId, doc.createdAt], doc);
}
});
window.fireproof = database; // 🤫 for dev
return database;
};
setupDatabaseFn
A note on using Context
If you are just calling useLiveQuery
and useDocument
and doing setup with the synchronous defineDatabaseFn
, you may not need to manage context. If you are doing async setup work with setupDatabaseFn
you will need to manage context. This allows you to run database setup code once for your entire app. Here is what you might see in App.js:
import { FireproofCtx, useFireproof } from '@fireproof/core/hooks/use-fireproof';
function App() {
// establish the Fireproof context value
const fpCtxValue = useFireproof('dbname', defineIndexes, setupDatabase);
// render the rest of the application wrapped in the Fireproof provider
return (
<FireproofCtx.Provider value={fpCtxValue}>
<MyComponent />
</FireproofCtx.Provider>
);
}
An asynchronous function that uses the database when it's ready, run this to load fixture data, insert a dataset from somewhere else, etc. Here's a simple example:
async function setupDatabase(database)) {
const apiData = await (await fetch('https://dummyjson.com/products')).json()
for (const product of apiData.products) {
await database.put(product)
}
}
If you are running the same setup across multiple users installations, you probably want to use deterministic randomness to generate the same data on each run, so people can sync together. Here is an example of generating deterministic fixtures, using mulberry32
for deterministic randomness so re-runs give the same CID, avoiding unnecessary bloat at development time, taken from the TodoMVC demo app.
function mulberry32(a) {
return function () {
let t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const rand = mulberry32(1); // determinstic fixtures
export default async function loadFixtures(database) {
const nextId = (prefix = '') => prefix + rand().toString(32).slice(2);
const listTitles = ['Building Apps', 'Having Fun', 'Getting Groceries'];
const todoTitles = [
[
'In the browser',
'On the phone',
'With or without Redux',
'Login components',
'GraphQL queries',
'Automatic replication and versioning',
],
['Rollerskating meetup', 'Motorcycle ride', 'Write a sci-fi story with ChatGPT'],
[
'Macadamia nut milk',
'Avocado toast',
'Coffee',
'Bacon',
'Sourdough bread',
'Fruit salad',
],
];
let ok;
for (let j = 0; j < 3; j++) {
ok = await database.put({
title: listTitles[j],
type: 'list',
_id: nextId('' + j),
});
for (let i = 0; i < todoTitles[j].length; i++) {
await database.put({
_id: nextId(),
title: todoTitles[j][i],
listId: ok.id,
completed: rand() > 0.75,
type: 'todo',
});
}
}
}