React Tutorial
This article will walk you through creating a simple todo list application from scratch. If you are new to React, it should be all you need to get started. If you are an expert, skim ahead and then check out the API docs.
If you are starting a fresh project, you might want to fork the Fireproof React Starter Kit. It has routes, authentication, and a basic data model already built, so you'll have a winning app in no time.
Create a New App
We'll start with a fresh React app. We'll use Vite, but we've tested with Next.js and it works great. We need help with Remix, and there's a known workaround if you prefer Create React App. If you have a favorite React starter, make a note on that issue and we'll add it to the list.
npm create vite@latest my-vite-fp-tutorial
# ✔ Select a framework: › React
# ✔ Select a variant: › TypeScript
cd my-vite-fp-tutorial
This app will manage todos. There's no schema to set up -- you can use TypeScript to enforce schemas, see the React TypeScript Starter Kit for examples. Instead we just start with a query that returns nothing, and a form that writes the documents to the ledger. In plain JavaScript apps, you can subscribe your redraw()
function to the ledger, or inspect the update stream and surgically update the parts of the page that need to change. In React, you don't have to worry about any of that, the hooks will do it for you.
The Data Model
A todo document looks like this:
{
_id: '018ad289-efc6-7c93-acaa-202cf4b3cdb7',
text: 'Learn Fireproof',
completed: false,
date: 1623937200000
}
The _id
is automatically set by Fireproof so you can ignore it -- it comes in handy for uniqueness constraints, for instance you could set the _id
equal to todo.text
and then you'd never have two todos with the same text (but not be able to edit the text without creating a new document). The date
field is used to sort the todos, and the completed
field is used to toggle the checkbox.
More complex apps might have a listId
on the todo, so you can group them into different lists. Then each list could be viewed independently.
Install Fireproof
Installing the React hooks package will also install the core Fireproof library. You don't need to run a server or install anything else, Fireproof includes connectors for S3 and IPFS.
npm install use-fireproof
Connect Your Component
In this example, our todo list application can create todo items, list them, and toggle their completed status. We'll start by modifying the component called App
in src/App.tsx
. This component is wired as the root of the application by Vite in src/main.tsx
so it's best to work within it. By the end of the tutorial we will have replaced the whole file, but take it one step at a time, to learn how the pieces fit together. The final file is shared below.
In this app, we use the top-level useLiveQuery
hook to auto-refresh query responses (so your app dynamically refreshes with no extra work), and the useDocument
hook to create new documents. These hooks can also be configured by the optional useFireproof
hook, but most apps should start with the defaults.
Import the Hooks
The first step is to import the hooks into your new app. In src/App.js
, add the following line to the top of the file:
import { useLiveQuery, useDocument } from 'use-fireproof'
These hooks are all you need to automatically initiate a browser-local copy of the ledger and begin development. The useLiveQuery
hook will automatically refresh query results, and the useDocument
hook loads and saves Fireproof documents and handles refreshing them when data changes.
Fireproof takes a build-first approach, so after your UI is running, you can connect to your cloud of choice. For now, let's build the app.
Query Todos
Now, inside of your component, you can call useLiveQuery
to get a list of todos (it will start empty):
function App() {
const response = useLiveQuery('date', {limit: 10, descending: true})
const todos = response.docs
In short, this is indexing the ledger by the date
field, and will ignore any documents that don't have a date
field. Queries will be sorted by date
. Learn more about queries in the index and query documentation.
The useLiveQuery
hook will automatically refresh the response
object when the ledger changes. The response object contains the docs
array, which is the list of todos. The response also has rows
which are the index rows, in this case they will have a key
with the date
field of the todo, and an id
field with the document id of the todo. In more complex applications you can customize the value
of these rows, for instance to provide full-name from first and last. Read more about indexes and queries in the documentation.
In our application, the todos are displayed by the following JSX, which renders their text
field. The event handler for updating the todo is written inline. Notice how ledger.put
is used to toggle the completed
field when the checkbox is clicked:
<ul>
{todos.map(todo => (
<li key={todo._id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => useLiveQuery.ledger.put({ ...todo, completed: !todo.completed })}
/>
{todo.text}
</li>
))}
</ul>
For convenience, the ledger
object is attached to the useLiveQuery
and useDocument
hooks. The ledger.put
function is used to update the document, and it will automatically refresh the query results. Read more in the document API documentation. In this tutorial, we'll also use the useDocument
hook to manage documents. The ledger.put
function is better for toggling the completed field, but useDocument
will be useful for creating new todos.
Create a New Todo
Next, we'll add a form to create new todos. Notice how useDocument
is called with an initial value for the document:
const [todo, setTodo, saveTodo] = useDocument({
text: "",
date: Date.now(),
completed: false,
});
The return value is essentially the return value of useState
but with a save document function added, in this case called saveTodo
. A very common pattern in React is to use a state variable and a setter function to manage the state of a form. This hook is a convenience for that pattern, but it also handles saving the document to the ledger. Follow the interactions in the code below to see how useDocument
is compatible with the patterns you're already using with useState
.
The useDocument
hook is used to create a new document with an empty text
field. The saveTodo
function is called when the form is submitted, and it saves the document to the ledger. The setTodo
function is used to update the text
field as the user types.
Save the Todo
Here is the JSX that renders the form. The common React pattern described above is used here: the input field is bound to todo.text
, setTodo
is called with a new text field when the input changes, and saveTodo
is called when the form is submitted, persisting the new todo to the ledger.
<div>
<input
type="text"
value={todo.text}
onChange={e => setTodo({ text: e.target.value })}
/>
<button
onClick={async () => {
await saveTodo()
setTodo()
}}
>
Save
</button>
</div>
Another convenience detail: setTodo
is called to clear the input field (and reset to the useDocument
call's initial value) after the todo is saved. This is a common pattern in React, and it's handled automatically by the hook. In our current application, we want the document managed by useDocument
to be a new one each time, so we do not specify an _id
in the initial document value. If the initial document value had an _id
field, the hook would update that document instead of creating a new one with each save. Read more about the useDocument
hook.
Where's My Data?
By default, Fireproof stores data in the browser's local storage. This is great for development, but once your app is ready to share, you'll want to connect it to cloud storage. For now, you can manage and delete the encrypted data from your browser developer tools. There are two components to the data, the header and the encrypted files. The header is kept in localStorage
under the key fp.useFireproof
. The files are stored in IndexedDB under the key fp.<keyId>.useFireproof
. This arrangement means that files can be stored with an untrusted provider, and the header can be stored securely. For instance, you can keep the encrypted data files in IPFS or a public S3 bucket, and keep the headers locally or in your applications session store.
Once your data is replicated to the cloud, you can view and edit it with the Fireproof developer tools. (See the Connect documentation for more information.)
The Completed App
Here's the example to-do list that initializes the ledger and sets up automatic refresh for query results. The list of todos will redraw for all users in real-time. Replace the code in src/App.js
with the following:
import {useDocument, useLiveQuery} from "use-fireproof";
import "./App.css";
function App() {
const response = useLiveQuery('date', {limit: 10, descending: true})
const todos = response.docs
const [todo, setTodo, saveTodo] = useDocument({
text: "",
date: Date.now(),
completed: false,
})
return (
<>
<input
title="text"
type="text"
value={todo.text as string}
onChange={(e) => setTodo({text: e.target.value})}
/>
<button
onClick={async () => {
await saveTodo()
setTodo()
}}
>
Save
</button>
<ul>
{todos.map((todo) => (
<li key={todo._id}>
<input
title="completed"
type="checkbox"
checked={todo.completed as boolean}
onChange={() =>
useLiveQuery.ledger.put({
...todo,
completed: !todo.completed,
})}
/>
{todo.text as string}
</li>
))}
</ul>
</>
)
}
export default App;
Run the App
Now take a look at your app. It will allow you to add items to the list and check the box.
npm run dev
You can clone the resulting application here.
Learn More
If you are starting a fresh project, you might want to fork the Fireproof React Starter Kit. It has routes, authentication, and a basic data model already built, so you can win a hackathon just by changing the parts you need.
Continue reading about how to integrate Fireproof with your existing authentication system, or check out the ChatGPT quickstart to learn how to use ChatGPT to rapidly prototype new applications with Fireproof.