React + Vite tutorial

Build a Todos app with React, Vite and Triplit

This tutorial will show you how to build a simple Todos app with React, Vite and Triplit. It will cover the following topics:

  • How to create a new Triplit project
  • How to create a new React app with Vite
  • How to create a Triplit schema for the Todos app
  • How to use the Triplit console
  • How to read and mutate data with Triplit
  • How to sync data with Triplit

The app will be built with:

If at any time you want to check your progress against the finished app, or if you get stuck, you can find the source code for the finished app here (opens in a new tab).

Project setup

Create a new Triplit project

Before we start building the app, we need to create a new Triplit project. You can do this by running the following command:

npm create triplit-app@latest todos

This command will guide you through the new project setup process. When given the option, select react as the UI framework.

Once the project setup is complete, you'll have a new directory, todos, that contains a basic Vite + Triplit app. Next, install the dependencies:

cd todos && npm install

The directory structure for your new project should look like this:

/triplit
  schema.ts
/public
  [static files]
/src
  [app files]
[other files]

Define the database schema

Triplit uses a schema to define the structure of your data. By using a schema, Triplit can validate your data at runtime and provide autocompletion in your editor.

We're going to set up a schema for the Todos app in the ./triplit/schema.ts file. Open the file and replace its contents with the following code:

./triplit/schema.ts
import { Schema as S } from '@triplit/db';
import { ClientSchema } from '@triplit/client';
 
export const schema = {
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      completed: S.Boolean({ default: false }),
      created_at: S.Date({ default: S.Default.now() }),
    }),
  },
} satisfies ClientSchema;

This schema defines a single collection, todos, with the following fields:

  • id: A unique identifier for the todo. Every Triplit collection must have an id field. Defaults to a random string if not specified.
  • text: A string that contains the todo text.
  • completed: A boolean value that indicates whether the todo is completed or not. Defaults to false if not specified.
  • created_at: A timestamp that indicates when the todo was created. Defaults to the current time if not specified.

Start the development server

Triplit provides a development server that you can use to test your app locally. To start the development server, run the following command:

npx triplit dev -i

This will start the sync server on port 6543 and a database console on port 6542. It will also output API tokens that your app will use to connect to the sync server. The -i option will cause the sync server to load the schema on startup.

💡

If you're using our hosted service, Triplit Cloud, you can use the API tokens and url shown on the dashboard (opens in a new tab) for your project page. The instructions in the rest of this tutorial are the same whether you're using Triplit Cloud or running the sync server locally.

Now's a good time to set up an .env file in the todos directory. This file will contain the API tokens that your app will use to connect to the sync server. Create a new file called .env in the todos directory and add the following:

.env
TRIPLIT_DB_URL=http://localhost:6543
TRIPLIT_ANON_TOKEN=replace_me
TRIPLIT_SERVICE_TOKEN=replace_me
# Replace `replace_me` with the tokens in the terminal where you ran `npx triplit dev`
 
VITE_TRIPLIT_SERVER_URL=$TRIPLIT_DB_URL
VITE_TRIPLIT_TOKEN=$TRIPLIT_ANON_TOKEN

Now restart the development server by pressing Ctrl + C and running npx triplit dev again.

We're basically done setting up Triplit. Now we can start building the app.

Building the app

Let's start building the app.

Getting started with Vite

Vite is a build tool that makes building apps fast and easy. We already installed Vite above, so let's start using it.

In a new terminal, in the todos directory, run the following command to start the Vite development server:

npm run dev

This will start the Vite development server on port 5173. You can now open your browser and navigate to http://localhost:5173 to see the app.

The Triplit client

Now that we have the development server running, let's integrate Triplit into our client code.

Triplit provides a client library that you can use to read and write data. Let's initialize it with our API tokens and the schema that we defined earlier. Create a new file in the src directory called client.ts and add the following code:

triplit/client.ts
import { schema } from './schema.js';
 
export const triplit = new TriplitClient({
  schema,
  serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL,
  token: import.meta.env.VITE_TRIPLIT_TOKEN,
});

Any time you want to read or write data, you'll import the triplit object from this file.

Optional: styling

The Vite app template comes with some basic styles. Feel free to replace them with your own styles or some of ours. Replace the contents of index.css with the following:

src/index.css
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
 
  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;
 
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}
 
.app {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  max-width: 400px;
  gap: 16px;
}
 
.todo {
  display: flex;
  gap: 1em;
  align-items: center;
  margin-bottom: 8px;
}
 
.btn {
  margin-left: 8px;
  border: none;
  border-radius: 0.25em;
  padding: 0.5em 1em;
  background-color: cornflowerblue;
  color: #fff;
  font-size: 1em;
  cursor: pointer;
}
 
.todo-input {
  border-radius: 0.25em;
  border-color: cornflowerblue;
  padding: 0.5em 1em;
  background-color: #242424;
  color: #fff;
  font-size: 1em;
}
 
.x-button {
  border: none;
  background-color: transparent;
  display: none;
}
.x-button:hover {
  filter: brightness(1.5);
}
 
.todo:hover .x-button {
  display: block;
}

Creating todos

The Vite app template comes with an App.tsx file that contains some components. Let's replace it with an <form/> component that creates a new todo.

Replace the contents of App.tsx with the following:

src/App.tsx
import React, { useState } from 'react';
import { triplit } from './client';
 
export default function App() {
  const [text, setText] = useState('');
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await triplit.insert('todos', { text });
    setText('');
  };
 
  return (
    <div className="app">
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="What needs to be done?"
          className="todo-input"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button className="btn" type="submit" disabled={!text}>
          Add Todo
        </button>
      </form>
    </div>
  );
}

This component renders a form with a text input and a submit button. When the form is submitted, it calls triplit.insert to create a new todo. It uses React to update the text input's value and clear it when the form is submitted.

💡

Notice that in the call to triplit.insert, we're omitting several of the fields we defined in our schema. That's because those missing fields, id, completed, and created_at, have default values. Triplit will automatically fill in those fields with their default values.

The Triplit console

We have a component that creates a new todo, but we still need to write some code that fetches the todos from the database and renders them on the page. If we want to test that our insertions are working without doing any more work, we can use the Triplit console.

When you ran npx triplit dev earlier, it started a Triplit console on port 6542. You can open the console by navigating to http://localhost:6542 in your browser. You should see a page that looks like this:

Triplit console

The console allows you to view and edit the contents of your database. You can use it to view the todos that you've created so far. You can also use it to create new todos or update existing ones. Add some todos from your Vite app, and watch them appear in the todos collection in the console. Then, in the console, click on a cell in the new rows that appear and update it.

💡

The Triplit console is super powerful. Not only can you mutate data, but you can apply filters, sorts, navigate relations and more.

Creating a todo component

Now that we have a way to create a new todo, let's create a component that renders a todo.

Create a new components directory inside the src directory and a file called Todo.tsx and add the following code:

src/components/Todo.tsx
import { Entity } from '@triplit/client';
import { schema } from '../../triplit/schema';
import { triplit } from '../../triplit/client';
 
type Todo = Entity<typeof schema, 'todos'>;
 
export default function Todo({ todo }: { todo: Todo }) {
  return (
    <div className="todo">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() =>
          triplit.update('todos', todo.id, async (entity) => {
            entity.completed = !todo.completed;
          })
        }
      />
      {todo.text}
      <button
        className="x-button"
        onClick={() => {
          triplit.delete('todos', todo.id);
        }}
      >

      </button>
    </div>
  );
}

In this component, we're rendering a checkbox and some text describing our todo. The Todo component takes a single prop, a Todo entity, and renders it. To get the Todo type from our schema we use the Entity generic type, and pass in our schema and the name of the collection ('todos') that we want a type for.

When the checkbox is clicked, we call triplit.update to update the todo's completed field. triplit.update takes three arguments: the name of the collection, the id of the entity to update, and a callback that updates the entity.

When the ❌ button is clicked, we call triplit.delete to delete the todo. triplit.delete takes two arguments: the name of the collection and the id of the entity to delete.

Rendering the todos

Now that we have a component that renders a todo, let's render a list of todos.

First, let's query the todos from the database. We're going to use the useQuery hook provided by Triplit to query the todos and store them as React state. At the top of App.tsx, add the following code:

src/App.tsx
import React, { useState } from 'react';
import { triplit } from '../triplit/client';
import { useQuery } from '@triplit/react';
 
function useTodos() {
  const todosQuery = triplit.query('todos').order('created_at', 'DESC');
  const { results: todos, error } = useQuery(triplit, todosQuery);
  return { todos, error };
}
 
export default function App() {
  const [text, setText] = useState('');
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await triplit.insert('todos', { text });
    setText('');
  };
 
  return (
    <div className="app">
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="What needs to be done?"
          className="todo-input"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button className="btn" type="submit" disabled={!text}>
          Add Todo
        </button>
      </form>
    </div>
  );
}

The useQuery hook takes two arguments: a Triplit client and a query. The query is created by calling triplit.query and passing in the name of the collection that we want to query. useQuery returns an object with the following properties:

  • results: A Map<string, Todo> that contains the results of the query. The keys of the map are the ids of the todos, and the values are the todos themselves.
  • error: An error object if the query failed, or undefined if the query succeeded.
  • fetching: A boolean that indicates whether the query is currently fetching data.
💡

One thing to notice is that we've added an order clause to the query. This will order the todos by their created_at field in descending order. This means that the most recently created todos will be at the top of the list. Triplit's query API supports a wide range of clauses, including where, limit, offset, order, and more. You can learn more about the query API here.

Now we're going to render the todos in the App component. Add the following lines to the App component:

src/App.tsx
import React, { useState } from 'react';
import { useQuery } from '@triplit/react';
import { triplit } from '../triplit/client';
import Todo from './components/Todo';
 
function useTodos() {
  const todosQuery = triplit.query('todos').order('created_at', 'DESC');
  const { results: todos, error, fetching } = useQuery(triplit, todosQuery);
  return { todos, error, fetching };
}
 
export default function App() {
  const [text, setText] = useState('');
  const { todos } = useTodos();
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await triplit.insert('todos', { text });
    setText('');
  };
 
  return (
    <div className="app">
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="What needs to be done?"
          className="todo-input"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button className="btn" type="submit" disabled={!text}>
          Add Todo
        </button>
      </form>
      {todos && (
        <div>
          {Array.from(todos).map(([id, todo]) => (
            <Todo key={id} todo={todo} />
          ))}
        </div>
      )}
    </div>
  );
}

Here we've:

  1. Imported our <Todo/> component
  2. Called the useTodos hook to query the todos
  3. Rendered the todos by turning the Map<string, Todo> returned by the useTodos hook into an Array, and iterated over the array with Array.map in our <App/> component.
💡

Triplit queries are live, meaning that you never need to manually refetch data. As other clients insert, update or delete data, your query will automatically update to reflect those changes. Even if you go offline, the query will listen for the changes that you make locally and update the query with those local changes. When you go back online, the Triplit will sync those local changes with the server and pull in any changes it missed while we were offline.

Persisting data

So far, Triplit has been storing data in-memory (the default for TriplitClient). That means that if you refresh the page and go offline, you'll lose your data. Triplit supports a variety of storage options that you can use to persist your data even between refreshes or if you go offline.

IndexedDB (opens in a new tab) is a low level durable storage API built into all modern browsers. Update the client.ts file to use the indexeddb storage option:

triplit/client.ts
import { schema } from './schema.js';
 
export const triplit = new TriplitClient({
  storage: 'indexeddb',
  schema,
  serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL,
  token: import.meta.env.VITE_TRIPLIT_TOKEN,
});

When you use the Triplit client to insert or update data, that data will persist to IndexedDB. Test it out: if you create a few todos and refresh your browser, you should see that your todos are still there.

Testing out the sync

Now that we have a working app, let's test out the sync. In one browser window, navigate to http://localhost:5173. Then, open a private browsing window and navigate to http://localhost:5173. You should see the same app in both tabs. Now, create a new todo in one tab. You should see the todo appear in the other tab. Try checking and unchecking the todo. You should see the changes reflected in the other tab. Triplit works offline as well - try disconnecting from the internet and creating a new todo in one of the windows. You should see the todo appear in the other tab when you reconnect. This is the power of syncing with Triplit!

Next steps

We've built a simple Todos app with React, Vite and Triplit. We've learned how to:

  • Create a new Triplit project
  • Create a new React app with Vite
  • Create a Triplit schema for the Todos app
  • Read and mutate data with Triplit
  • Sync data with Triplit

And there are still a lot of things that we haven't covered.

If you have any questions, feel free to reach out to us on Discord (opens in a new tab) or Twitter (opens in a new tab).