In this guide, you will:
- Create a new local-first project using create-lofi-app
- Create a simple To-Do UI
- Connect to a Basic project and set up your schema
- Add sign in and sign out functionality
- Hook up to Basic DB so that your users can save their to-dos across their devices
Create a new local-first project
Create a new local-first project
# create a new local-first project:
npx create-lofi-app
When prompted in the CLI, enter y
for installing the vite package.
Then give your project a name.
And finally choose the Basic + Tailwind variant.
install npm packages
cd project-name
into your project, run npm i
, and start your server with npm run dev
:
# first, cd into your project
cd project-name
# then, install all packages
npm i
# start your server
npm run dev
You’ll now be able to navigate to http://localhost:5173
and see the app running as below:
Create a simple To-Do UI
We’ll replace the existing homepage UI with a simple To-Do app UI using Tailwind that comes preinstalled with the create-lofi-app template.
Delete the existing homepagecode
Let’s replace the code in your src/App.tsx
file with the following for a clean slate:
import './App.css'
function App() {
return (
<>
<h1>my lofi to-do app</h1>
</>
)
}
export default App
Add an input + button
Let’s add an input, and a button to the app, and add some basic Tailwind styling
import './App.css'
function App() {
return (
<div className="flex flex-col items-center">
<h1 className="mb-4">my lofi to-do app</h1>
<div className="flex gap-2">
<input
type="text"
placeholder="Add a to-do"
className="border rounded px-2 py-1"
/>
<button className="bg-blue-500 px-3 py-1 rounded">
Add
</button>
</div>
</div>
)
}
export default App
You should now see a simple input and button on the page:
Connect to a Basic project and set up your schema
The fastest way to connect to a Basic project is to use the Basic CLI.
Install the Basic CLI
Make sure you are in the directory of your project.
# install the basic cli
npm install -g @basictech/cli
# create a Basic account and login
basic login
You’ll be redirected in your browser to sign up for a Basic account and login.
Once you’ve logged in, you’ll be able to connect to a Basic project.
Connect to a Basic project
# connect to a Basic project
basic init
Choose to Create new project
You’ll then be prompted to give your Basic project a name (this doesn’t have to match your directory name, but it’s best to keep it consistent), and to select a project template.
Choose the typescript
and hit Yep!
Set up your schema
If you navigate to the src/basic.config.ts
file, you’ll see that it’s already set up for you now with a schema object with a project_id
!
Let’s change the schema to match the needs of our To-Do app, and add a name string field and a completed boolean field.
export const schema = {
project_id: "your-project-id",
version: 0,
tables: {
todos: {
type: "collection",
fields: {
name: {
type: "string"
},
completed: {
type: "boolean"
}
}
}
}
}
Push the changes to our schema
# push the changes to our schema
basic push
Add sign in and sign out functionality
It’s very easy to add the ability to have your users sign in and sign out of your to-do app.
Add a login and logout button to App.tsx
Now navigate to src/App.tsx
and import signin
, signout
, isSignedIn
, and user
.
/// add back the UseBasic import
import { useBasic } from '@basictech/react'
import './App.css'
function App() {
// Import signin to enable the user to sign in
// Import signout to enable the user to sign out
// Import isSignedIn to check if the user is signed in
// Import user to get the user's information once they're signed in
const { signin, isSignedIn, user, signout } = useBasic()
return (
<div className="flex flex-col items-center">
<h1 className="mb-4">my lofi to-do app</h1>
<div className="flex gap-2">
<input
type="text"
placeholder="Add a to-do"
className="border rounded px-2 py-1"
/>
<button className="bg-blue-500 px-3 py-1 rounded">
Add
</button>
</div>
{/* if not signed in yet, show the sign in button */}
{/* when signed in, show the user's email */}
{isSignedIn ? (
<div className="mt-12">
<p>Signed in as: {user?.email}</p>
{/* Add a button to sign out */}
<button" onClick={signout}>Sign Out</button>
</div>
) : (
<button className="mt-12" onClick={signin}>Sign In</button>
)}
</>
)
}
export default App
All of it put together should look like this when signed in:
Hook up to Basic DB
Now that we have the ability to sign in and sign out, we can hook up to Basic DB so that our users can save their to-dos across their devices.
Submit a task to Basic DB
We will add some state to capture our input value, implement the .add()
method, and then console.log
the result.
import { useBasic } from '@basictech/react'
// import useState
import { useState } from 'react'
import './App.css'
function App() {
// add the db import
const { signin, signout, isSignedIn, user, db } = useBasic()
// add the taskInput state
const [taskInput, setTaskInput] = useState('')
// add the addTask function
const addTask = () => {
if (!taskInput.trim()) return // only runs if the taskInput is not empty
db.collection('todos').add({
name: taskInput,
completed: false
})
.then((item: any) => {
setTaskInput('') // Clear input after successful add
console.log("Successfully added new todo with ID: ", item)
})
.catch((error: any) => {
console.error("Error adding todo: ", error)
})
}
return (
<div className="flex flex-col items-center">
<h1 className="mb-4">my lofi to-do app</h1>
<div className="flex gap-2">
<input
type="text"
placeholder="Add a to-do"
className="border rounded px-2 py-1"
// add the value and onChange states so we can capture the input
value={taskInput}
onChange={(e) => setTaskInput(e.target.value)}
/>
{/* add the addTask function to the button */}
<button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
Add
</button>
</div>
{isSignedIn ? (
<div className="mt-12">
<p>Signed in as: {user?.email}</p>
<button onClick={signout}>Sign Out</button>
</div>
) : (
<button className="mt-12" onClick={signin}>Sign In</button>
)}
</div>
)
}
export default App
Read and display the tasks
Now let’s retrieve and display the tasks!
/ add useQuery hook import
import { useBasic, useQuery } from '@basictech/react'
import { useState } from 'react'
import './App.css'
function App() {
const { signin, signout, isSignedIn, user, db } = useBasic()
const [taskInput, setTaskInput] = useState('')
// get all tasks from the database
const tasks = useQuery(() => db.collection('todos').getAll())
const addTask = () => {
if (!taskInput.trim()) return
db.collection('todos').add({
name: taskInput,
completed: false
})
.then((item: any) => {
setTaskInput('')
console.log("Successfully added new todo with ID: ", item)
})
.catch((error: any) => {
console.error("Error adding todo: ", error)
})
}
return (
<div className="flex flex-col items-center">
<h1 className="mb-4">my lofi to-do app</h1>
<div className="flex gap-2">
<input
type="text"
placeholder="Add a to-do"
className="border rounded px-2 py-1"
value={taskInput}
onChange={(e) => setTaskInput(e.target.value)}
/>
<button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
Add
</button>
</div>
{/* Add the tasks list here */}
<div className="mt-4 w-full max-w-md">
{tasks?.map((task: any) => (
<div
key={task.id}
className="flex items-center p-2"
>
<span>{task.name}</span>
</div>
))}
</div>
{isSignedIn ? (
<div className="mt-12">
<p>Signed in as: {user?.email}</p>
<button onClick={signout}>Sign Out</button>
</div>
) : (
<button className="mt-12" onClick={signin}>Sign In</button>
)}
</div>
)
}
export default App
With just a few lines you were able to display your tasks as such:
Add a delete button
Now let’s add a delete button to our tasks using the .delete()
method.
import { useBasic, useQuery } from '@basictech/react'
import { useState } from 'react'
import './App.css'
function App() {
const { signin, signout, isSignedIn, user, db } = useBasic()
const [taskInput, setTaskInput] = useState('')
const tasks = useQuery(() => db.collection('todos').getAll())
const addTask = () => {
if (!taskInput.trim()) return
db.collection('todos').add({
name: taskInput,
completed: false
})
.then((item: any) => {
setTaskInput('')
console.log("Successfully added new todo with ID: ", item)
})
.catch((error: any) => {
console.error("Error adding todo: ", error)
})
}
// add the deleteTask function
const deleteTask = (id: string) => {
db.collection('todos').delete(id)
}
return (
<div className="flex flex-col items-center">
<h1 className="mb-4">my lofi to-do app</h1>
<div className="flex gap-2">
<input
type="text"
placeholder="Add a to-do"
className="border rounded px-2 py-1"
value={taskInput}
onChange={(e) => setTaskInput(e.target.value)}
/>
<button className="bg-blue-500 px-3 py-1 rounded" onClick={addTask}>
Add
</button>
</div>
{/* update styling to add a delete button */}
<div className="mt-4 w-full max-w-md">
{tasks?.map((task: any) => (
<div
key={task.id}
className="flex items-center justify-between p-2 border-b"
>
<span>{task.name}</span>
<button
onClick={() => deleteTask(task.id)}
className="text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
))}
</div>
{isSignedIn ? (
<div className="mt-12">
<p>Signed in as: {user?.email}</p>
<button onClick={signout}>Sign Out</button>
</div>
) : (
<button className="mt-12" onClick={signin}>Sign In</button>
)}
</div>
)
}
export default App
The delete button would look like this:
And that’s it! You’ve now built a local-first to-do app that allows your users to sign in, sign out, and save their to-dos across their devices.
You’ll also notice how snappily all the changes are reflected on the app, from task creation to deletion - this is the power of local-first architecture!