How to Build a React Todo App with Bazaar
In this tutorial, you will build a simple Todo application using React and Bazaar. This guide will walk you through setting up a new React project, integrating Bazaar for backend services, and deploying your application to Netlify. By the end of this tutorial, you will have a fully functional Todo app with features to add, display, mark as complete, and delete tasks.
Prerequisites
To complete this tutorial, you will need:
- A basic understanding of React.
- Node.js installed on your machine.
- Git installed on your machine.
- A GitHub account for code repository hosting.
- (Optional) A Netlify account for deploying your app.
Step 1 — Scaffolding a New React Project
In this step, you will create a new React project with TypeScript using the official Vite project scaffolding tool, create-vite
.
Open up your terminal and ensure your current working directory is where you intend to create a project. Then, Run the following command:
npm create vite@latest bazaar-react-todo -- --template react-ts
That command will install and execute create-vite
, creating your React project in a folder named bazaar-react-todo
. The option , react-ts
, is the React + TypeScript template preset.
Next, navigate to your project directory, install dependencies, and start the development server:
cd bazaar-react-todonpm installnpm run dev
Open the default URL http://localhost:5173/
in your browser to see the starter React project.
In the next step, you’ll prepare your project by cleaning up unnecessary scaffolded files and adding custom styles.
Step 2 — Preparing the Project
In this step, you will clean up the scaffolded React project by removing unnecessary files and adding custom styles.
-
First, delete the following files and folders:
src/App.css
src/assets/react.svg
-
Replace the contents of
src/assets/index.css
with:index.css body {font-family: sans-serif;}.container {margin: 0 auto;max-width: 600px;padding: 3em 0;}.card {background-color: #eee;padding: 1rem;}.todo-form {margin: 2rem 0;}.todos {display: flex;flex-direction: column;gap: 1rem;}.todo-text {margin-bottom: 1rem;}.strikethrough {text-decoration: line-through;} -
Finally, replace the contents of
src/App.tsx
with the following:App.tsx function App() {return (<><div className="container"><h1>Todo</h1>{/* Todo template content will be implemented here */}</div></>);}export default App;
Check your app in the browser to see the text Todo.
With the project cleaned up, you are ready to integrate Bazaar for authentication and realtime data storage.
Step 3 — Running the Mock Server
In this step, you will use the Bazaar mock server to develop your app locally. The mock server simulates the Bazaar backend, allowing you to test and build your application in a local environment.
In a new terminal window, run the following command to start the mock server:
npx @bzr/bazaar-mock -u http://localhost:5173/
This command assumes the Vite dev server is running on the default port 5173
. Update accordingly if you’re running on a different port.
Install the @bzr/bazaar-mock
NPM package if prompted.
You should see output similar to this:
Running mock Bazaar server at: http://localhost:3377
App ID is testApp URL is http://localhost:5173/Redirect URIs are http://localhost:5173/Use any email to sign up / log in. Code is always 1234
Now that the mock server is running, you’ll initialize a Bazaar client.
Step 4 — Initializing a Bazaar Client
In this step, you will install and initialize the Bazaar client to manage backend services for your Todo app.
First, install the Bazaar library:
npm install @bzr/bazaar
Next, initialize the Bazaar client at the top of src/App.tsx
:
import { BazaarApp } from "@bzr/bazaar";
const bzr = new BazaarApp({ appId: import.meta.env.VITE_APP_ID || "test", loginRedirectUri: import.meta.env.VITE_URL || window.location.href, onApiConnectError: async (bzr) => bzr.logOut(),});
// Test the client initialization by checking the logged in statusconsole.log("is logged in:", bzr.isLoggedIn());
function App() {...}
In this code, you set the following BazaarApp
options:
appId
: Checks for aVITE_APP_ID
environment variable and, if not present, usestest
, the Bazaar mock server default.loginRedirectUri
: Checks for aVITE_URL
environment variable and, if not present, useswindow.location.href
as the default, e.g.http://localhost:5173/
.onApiConnectError
: If a connection error occurs, logs out the user.
Open the app in your browser and check the developer console. You’ll see the following:
is logged in: false
With the Bazaar client initialized you can now add authentication to your app.
Step 5 — Adding User Authentication
In this step, you’ll add user authentication to your app using Bazaar’s OAuth2-based authentication system.
First, add log in and log out buttons to your template:
<div className="container"> <h1>Todo</h1> {/* Todo template content will be implemented here */} <button onClick={() => bzr.login()}>Log in or create account</button> <button onClick={() => bzr.logOut()}>Log out</button></div>
When a user clicks the log in button, a pop-up window opens, prompting them to create a Bazaar account or log in if they already have one. Once the account is created, the user is prompted to authorize your Todo app.
Create a test account by entering any email address, using the code 1234
and choosing any handle and name. Then, authorize the app. The pop-up will close. Refresh the page, and in your developer console you’ll see the following:
is logged in: true
Next, refactor the code so the login status updates immediately after a successful login, not just on page load.
import { BazaarApp } from "@bzr/bazaar";import { useEffect } from "react";
// Test the client initialization by checking the logged in statusconsole.log("is logged in:", bzr.isLoggedIn());
function App() { useEffect(() => { async function doLoginActions() { console.log("is logged in:", bzr.isLoggedIn()); }
// Perform login actions if the user is already logged in on page load if (bzr.isLoggedIn()) { doLoginActions(); }
// Set up handler to perform login actions immediately after a successful login bzr.onLogin(doLoginActions); }, []);
...}
In these changes, you wrap the login code in useEffect
with an empty dependency array so the code is executed only on the component’s initial render. The code needs to be in the component function because you will use reactive state in the doLoginActions
function in the next step.
Log out and log back in, and you’ll see is logged in: true
in your console after the pop-up closes without needing a page refresh.
You have now added user authentication to your app. Next, you will conditionally display content based on the user’s login status.
Step 6 — Displaying Logged-in Content
In this step, you will conditionally display content based on the user’s login status.
First, make the following changes:
import { BazaarApp } from "@bzr/bazaar";import { useEffect, useState } from "react";...
function App() { const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => { async function doLoginActions() { console.log("is logged in:", bzr.isLoggedIn()); setIsLoggedIn(true); }
... }, []);
return ( <> <div className="container"> <h1>Todo</h1> {!isLoggedIn ? ( <button onClick={() => bzr.login()}>Log in or create account</button> ) : ( <button onClick={() => bzr.logOut()}>Log out</button> )} </div> </> );}
In these changes, you create a state variable for the login status and set its value to true
when a user logs in. Next, you update the template to display log in and log out buttons conditionally.
Next, you’ll add the functionality to create todos.
Step 7 — Adding a Todo
In this step, you’ll implement the feature to create todos.
Make the following changes to your script:
import { BazaarApp } from "@bzr/bazaar";import { useEffect, useState, FormEvent } from "react";
type Todo = { id: string; text: string; completed: boolean;};
const bzr = new BazaarApp({...});
const todosCollection = bzr.collection<Todo>("todos");
function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); const [todoText, setTodoText] = useState(""); const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => {...}, []);
async function onSubmit(event: FormEvent) { event.preventDefault();
const unpersistedTodo = { text: todoText, completed: false }; const id = await todosCollection.insertOne(unpersistedTodo); setTodos([...todos, { id, ...unpersistedTodo }]);
setTodoText(""); }}
In this code, you define a Todo
type and create two state variables, one for the new todo text and another for holding the list of todos.
The bzr.collection
method creates an object to interact with a database collection. Learn more in the collections docs.
The onSubmit
function adds a new todo to the todos
collection with todosCollection.insertOne
(collections are created lazily) and adds the todo to your app’s local state with the ID generated by the database after insertion.
Next, add the form to your template after the log out button:
{!isLoggedIn ? ( <button onClick={() => bzr.login()}>Log in or create account</button>) : ( <> <button onClick={() => bzr.logOut()}>Log out</button>
<form onSubmit={onSubmit} className="todo-form"> <input type="text" value={todoText} onChange={(event) => setTodoText(event.target.value)} placeholder="Write a todo..." required /> <button>Add</button> </form> </>)}
In these changes, you hook up the onSubmit
handler to the form submit event and bind the todoText
state variable you just created to the input. There are now multiple logged-in elements, which get wrapped in a fragment (<></>
).
You have now implemented the functionality to add new todos, but they still need to be displayed.
In the next step, you will display these todos in your app.
Step 8 — Displaying Todos
In this step, you will fetch and display todos from the collection.
First, update the doLoginActions
function to fetch todos:
async function doLoginActions() { setIsLoggedIn(true); todosCollection.getAll().then((docs) => setTodos([...docs]));}
In this code, todosCollection.getAll()
fetches all the todos from the collection, and the results are stored in the todos
state variable.
Next, update the template to display the todos under the form:
<form onSubmit={onSubmit} className="todo-form">...</form>
<div className="todos"> {todos.map((todo) => ( <div className="card" key={todo.id}> <div className="todo-text">{todo.text}</div> </div> ))}</div>
Add a few more todos in your browser and notice they get added to the bottom of your list. Usually you want the newest todos to appear first. To do that, you’ll reverse the fetched todos.
Start by adding reversedTodos
, which derives its state from todos
:
...const [todos, setTodos] = useState<Todo[]>([]);
const reversedTodos: Todo[] = [...todos].reverse();...
The spread operator (...
) creates a new array, as the reverse
method mutates an array.
Then, in your template, replace todos
with the new reversedTodos
state variable to list the todos:
...<div className="todos"> {reversedTodos.map((todo) => ( <div className="card" key={todo.id}> <div className="todo-text">{todo.text}</div> </div> ))}</div>...
You have now successfully displayed the todos fetched from the collection. In the next step, you will add realtime syncing to keep the todos updated across different devices.
Step 9 — Adding Realtime Syncing
In this step, you will enable realtime updates for your todos so that changes are reflected instantly across different devices.
Add the following to the doLoginActions
function:
async function doLoginActions() { setIsLoggedIn(true); todosCollection.getAll().then((docs) => setTodos([...docs]));
todosCollection.subscribeAll( {}, { onAdd: (doc) => { setTodos((todos) => { if (todos.some((todo) => todo.id === doc.id)) return [...todos]; return [...todos, doc]; }); }, } );}
In this code, todosCollection.subscribeAll
subscribes to all changes in the todos
collection. You pass a subscribe listener as the second subscribeAll
argument with a function to handle onAdd
events. When an onAdd
event is triggered, you update your local state.
Learn about subscribe listeners.
With the onAdd
handler in place, there’s no need to add a todo to your local state when the form is submitted. Replace the submit event listener with the following:
function onSubmit(event: FormEvent) { event.preventDefault();
const unpersistedTodo = { text: todoText, completed: false }; todosCollection.insertOne(unpersistedTodo);
setTodoText("");}
The main line removed is setTodos([...todos, { id, ...unpersistedTodo }]);
. The function no longer needs to be async as you don’t need to wait for the ID from the database and is updated accordingly.
To test the realtime updates, open your app in a private window while keeping the original window open. When you make changes in one window, they will appear in the other in realtime.
Realtime syncing is enabled, but it only handles add todo events. In the next step, you will implement the functionality to toggle the completion status of todos and update your subscribe listener with an onChange
handler.
Step 10 — Toggling Todo Completion
In this step, you’ll implement functionality to toggle the completion status of todos.
First, add an onChange
handler to your subscribe listener:
todosCollection.subscribeAll( {}, { onAdd: (doc) => {...}, onChange: (_, newDoc) => { setTodos((todos) => todos.map((todo) => (todo.id === newDoc.id ? newDoc : todo)) ); }, },);
The onChange
handler updates the corresponding todo in your local state. Note that the first parameter in the onChange
handler is newDoc
, which you don’t need. An underscore is used as the parameter name to indicate the variable is unused and to avoid a React build error.
Next, update your template:
<div className="todos"> {reversedTodos.map((todo) => ( <div className={`card ${todo.completed && "strikethrough"}`} key={todo.id} > <div className="todo-text">{todo.text}</div> <button onClick={() => toggleTodoCompletion(todo)}> Mark as {todo.completed ? "Incomplete" : "Completed"} </button> </div> ))}</div>
In these changes, you add a strikethrough
class to a todo when it’s complete. The strikethrough
style is already in index.css
.
And add the toggleTodoCompletion
click event handler below onSubmit
:
function onSubmit(event: FormEvent) {...}
function toggleTodoCompletion(todo: Todo) { todosCollection.updateOne(todo.id, { completed: !todo.completed });}
That function updates the completed
value for a todo, setting it to the opposite of its current state.
You can now mark todos as completed or incomplete. In the next step, you will add the functionality to delete todos.
Step 11 — Deleting a Todo
In this step, you will implement the functionality to delete todos.
First, add an onDelete
handler to your subscribe listener:
todosCollection.subscribeAll( {}, { onAdd: (doc) => {...}, onChange: (_, newDoc) => {...}, onDelete: (doc) => { setTodos((todos) => todos.filter((todo) => todo.id !== doc.id)); }, },);
The onDelete
handler deletes the corresponding todo from your local state.
Next, add a delete button to your template:
<div className="todos"> {reversedTodos.map((todo) => ( <div className={`card ${todo.completed && "strikethrough"}`} key={todo.id} > <div className="todo-text">{todo.text}</div> <button onClick={() => toggleTodoCompletion(todo)}> Mark as {todo.completed ? "Incomplete" : "Completed"} </button> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </div> ))}</div>
And add the toggleTodoCompletion
click event handler below deleteTodo
:
function toggleTodoCompletion(todo: Todo) {...}
function deleteTodo(id: string) { if (!window.confirm("Delete todo forever?")) return; todosCollection.deleteOne(id);}
Now you can delete todos.
Show the Completed App.tsx
File
import { BazaarApp } from "@bzr/bazaar";import { useEffect, useState, FormEvent } from "react";
type Todo = { id: string; text: string; completed: boolean;};
const bzr = new BazaarApp({ appId: import.meta.env.VITE_APP_ID || "test", loginRedirectUri: import.meta.env.VITE_URL || window.location.href, onApiConnectError: async (bzr) => bzr.logOut(),});
const todosCollection = bzr.collection<Todo>("todos");
function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); const [todoText, setTodoText] = useState(""); const [todos, setTodos] = useState<Todo[]>([]);
const reversedTodos: Todo[] = [...todos].reverse();
useEffect(() => { async function doLoginActions() { setIsLoggedIn(true); todosCollection.getAll().then((docs) => setTodos([...docs]));
todosCollection.subscribeAll( {}, { onAdd: (doc) => { setTodos((todos) => { if (todos.some((todo) => todo.id === doc.id)) return [...todos]; return [...todos, doc]; }); }, onChange: (_, newDoc) => { setTodos((todos) => todos.map((todo) => (todo.id === newDoc.id ? newDoc : todo)) ); }, onDelete: (doc) => { setTodos((todos) => todos.filter((todo) => todo.id !== doc.id)); }, } ); }
// Perform login actions if the user is already logged in on page load if (bzr.isLoggedIn()) { doLoginActions(); }
// Set up handler to perform login actions immediately after a successful login bzr.onLogin(doLoginActions); }, []);
function onSubmit(event: FormEvent) { event.preventDefault();
const unpersistedTodo = { text: todoText, completed: false }; todosCollection.insertOne(unpersistedTodo);
setTodoText(""); }
function toggleTodoCompletion(todo: Todo) { todosCollection.updateOne(todo.id, { completed: !todo.completed }); }
function deleteTodo(id: string) { if (!window.confirm("Delete todo forever?")) return; todosCollection.deleteOne(id); }
return ( <> <div className="container"> <h1>Todo</h1> {!isLoggedIn ? ( <button onClick={() => bzr.login()}>Log in or create account</button> ) : ( <> <button onClick={() => bzr.logOut()}>Log out</button>
<form onSubmit={onSubmit} className="todo-form"> <input type="text" value={todoText} onChange={(event) => setTodoText(event.target.value)} placeholder="Write a todo..." required /> <button>Add</button> </form>
<div className="todos"> {reversedTodos.map((todo) => ( <div className={`card ${todo.completed && "strikethrough"}`} key={todo.id} > <div className="todo-text">{todo.text}</div> <button onClick={() => toggleTodoCompletion(todo)}> Mark as {todo.completed ? "Incomplete" : "Completed"} </button> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </div> ))} </div> </> )} </div> </> );}
export default App;
With that, your todo app is complete and ready to deploy.
Step 12 — Deploying to Netlify
In this step, you will deploy your React Todo app to Netlify. You can deploy a Bazaar-powered app anywhere static sites can be hosted. Netlify provides a frictionless deployment process and free static site hosting for public GitHub repos.
First, initialize a Git repository and push your code to GitHub:
git initgit add .git commit -m "Initial commit: React todo app"
Go to GitHub and create a public repo named bazaar-react-todo
.
git remote add origin https://github.com/<your-username>/bazaar-react-todo.gitgit branch -M maingit push -u origin main
Next, go to Netlify, create an account if necessary, and click Add new site. Select Import an existing project and choose GitHub. Select your repository bazaar-react-todo
. If you can’t find your repo, click Configure the Netlify app on GitHub to grant access.
Configure the site settings and build settings as follows:
- Site name: e.g.
bazaar-react-todo
- Branch to deploy: e.g.
main
- Build command:
npm run build
(default) - Publish directory:
dist
(default)
Add environment variables VITE_URL
and VITE_APP_ID
. For VITE_URL
, use your app’s Netlify URL (e.g., https://bazaar-react-todo.netlify.app
).
To get your Bazaar app ID, open cloud.bzr.dev and create a free account. Click on the user icon at the top right, then click Developers. Next, click Create App.
Give your app a name. I’m using React Todo. Set your APP URL, which should be the same as your app’s Netlify URL, e.g. https://bazaar-react-todo.netlify.app
. Finally, click Create, then copy your app ID. It will look something like 01907291-d4e5-79d5-a312-ba20260c9b98
.
Return to Netlify and paste in your app ID as the VITE_APP_ID
value, and click Deploy. You’ll see a notice, Site deploy in progress. The process will take a few minutes, after which your todo app will be deployed!
Conclusion
In this tutorial, you built and deployed a Todo app using React and Bazaar. You set up a new React project, integrated Bazaar for backend services, implemented user authentication, added and displayed todos, enabled realtime syncing, and deployed your app to Netlify.
Check the Bazaar documentation for more features and capabilities.