Task Tracker (Web)
On this page
- Prerequisites
- A. Clone the Client App Repository
- B. Explore the App Structure & Components
- C. Connect to Your MongoDB Realm App
- D. Define the GraphQL Schema & Operations
- E. Connect Apollo to the GraphQL API
- F. Implement the Projects List
- G. Use Realm functions
- H. Try It Out Locally
- I. Deploy the App
- What's Next?
If you prefer to explore on your own rather than follow a guided tutorial, check out the Web Quick Start. It includes copyable code examples and the essential information that you need to set up a MongoDB Realm application.
In this tutorial, you'll build a functional web application backed by the MongoDB Realm GraphQL API. The app uses React to define UI components and Apollo Client to run queries and mutations through the GraphQL API. We've already created most of the frontend application for you, so you don't need to know React to follow along.
The app is a task tracker that allows users to:
- Register and log in with an email/password account.
- View a list of their projects.
- Add, view, and delete tasks for projects they are a member of.
- Switch tasks between Open, In Progress, and Complete statuses
- Add, view, and remove team members from projects.
This tutorial should take around 30 minutes.
The Realm Web SDK does not support sync, so the app you'll build in this tutorial uses other means to get live updates on data changes from the backend. However, data that you create from the web app will automatically sync to any of the other task tracker tutorial apps that use Realm Database.
This tutorial covers two ways to synchronize data from the server to the Web SDK:
- The
watch()
method as used in the Implement the Project List section. - Apollo GraphQL client polling as used in the Define the GraphQL Schema & Operations section.
Prerequisites
Before you get started, you'll need the following:
- A complete task tracker app in MongoDB Realm. If you haven't yet completed Set up the Task Tracker Tutorial Backend, do that before you continue.
- Node.js installed on your machine.
- git installed on your machine.
Once you're set up with these prerequisites, you're ready to start the tutorial.
A. Clone the Client App Repository
We've already put together a task tracker browser application that has most of the frontend code you'll need. You can clone the client application repository directly from GitHub:
git clone --branch start https://github.com/mongodb-university/realm-tutorial-web.git
In your terminal, run the following commands to navigate to the task tracker client application and install its dependencies:
cd realm-tutorial-web npm install
The start
branch is an incomplete version of the app that we will
complete in this tutorial. To view the finished app, check out the
final
branch and update src/App.js
with your Realm app
ID.
B. Explore the App Structure & Components
The web client is a standard React web application written in JavaScript and scaffolded with Create React App. We encourage you to explore the files in the app for a few minutes before you continue the tutorial. This will help you to familiarize yourself with what the app contains and where you'll be working.
The project uses the following file structure:
src/ ├── index.js ├── App.js ├── RealmApp.js ├── TaskApp.js ├── components/ │ ├── ButtonGroup.js │ ├── Card.js │ ├── EditPermissionsModal.js │ ├── Loading.js │ ├── LoginScreen.js │ ├── ProjectScreen.js │ ├── SideBar.js │ ├── StatusChange.js │ ├── TaskContent.js │ ├── TaskDetailModal.js │ └── useChangeTaskStatusButton.js └── graphql/ ├── RealmApolloProvider.js ├── useProjects.js ├── useTaskMutations.js ├── useTaskQueries.js └── useTasks.js
Apollo Client & GraphQL Operations
The /src/graphql
directory contains all of the modules that you'll use to
configure the Apollo GraphQL client and connect to your Realm app's GraphQL API.
These files are only incomplete scaffolds - some are blank and others require
you to make some modifications. This tutorial walks through adding the missing
code in these files to connect the task tracker app to Realm.
React Components & Hooks
The /src/components
directory contains pre-built React components and hooks
that handle local state management and UI rendering. The components import code
from the files in /src/graphql
and use them to interact with Realm. We've
already completely implemented the UI portions, so you won't need to add any
React-specific code to these files. We'll make sure to show you along the way
how to modify the components and hooks to use the Realm GraphQL API.
React is a popular modern web application framework that uses a component model to maintain application state and intelligently render pieces of the UI. If you're not familiar with React or want to brush up on your knowledge, check out the official React website, which has excellent documentation and tutorials.
C. Connect to Your MongoDB Realm App
The client app needs to connect to your Realm app so that users can register and
log in. In src/RealmApp.js
, we import the Realm Web SDK to connect to
Realm and handle these actions. The file exports a React context provider that
encapsulates this behavior and makes it available to other components in the
app.
Some of the functionality in RealmApp.js
is not fully defined. You need to
update the code to use the SDK to connect to your Realm app and handle user
authentication.
Create a Realm App Client
The app client is the primary interface to your Realm app from the SDK. In
src/App.js
, replace "TODO"
with your Realm App ID:
export const APP_ID = "<your Realm app ID here>";
Make sure to replace "TODO"
with your app's unique
App ID. You can find your App ID by clicking the
copy button next to the name of your app in the lefthand navigation of the
Realm UI.

Complete the Registration & Authentication Functions
The app client provides methods that allow you to authenticate and register
users through the email/password authentication provider. In
src/RealmApp.js
, the RealmAppProvider
component wraps these functions
and keeps the app client in sync with local React state.
These wrapping functions already have the state update calls but don't currently use the app client you created. You need to update the functions to actually call the SDK authentication and registration methods.
export const RealmAppProvider = ({ appId, children }) => { const [app, setApp] = React.useState(new Realm.App(appId)); React.useEffect(() => { setApp(new Realm.App(appId)); }, [appId]); // Wrap the Realm.App object's user state with React state const [currentUser, setCurrentUser] = React.useState(app.currentUser); async function logIn(credentials) { await app.logIn(credentials); // If successful, app.currentUser is the user that just logged in setCurrentUser(app.currentUser); } async function logOut() { // Log out the currently active user await app.currentUser?.logOut(); // If another user was logged in too, they're now the current user. // Otherwise, app.currentUser is null. setCurrentUser(app.currentUser); } const wrapped = { ...app, currentUser, logIn, logOut }; return ( <RealmAppContext.Provider value={wrapped}> {children} </RealmAppContext.Provider> ); };
In src/App.js
, we use the useRealmApp()
hook to determine
when the main application is ready to render. We also check for an
authenticated user and always render exclusively the login screen unless a
user is logged in. This guarantees that only authenticated users can access
the rest of the app.
const RequireLoggedInUser = ({ children }) => { // Only render children if there is a logged in user. const app = useRealmApp(); return app.currentUser ? children : <LoginScreen />; };
In /components/LoginScreen.js
, we use the wrapped authentication methods
that you defined to log user in and register new users.
Find the the handleLogin
function and add the following code to process a
emailPassword
credential by calling the logIn()
method.
const handleLogin = async () => { setIsLoggingIn(true); setError((e) => ({ ...e, password: null })); try { await app.logIn(Realm.Credentials.emailPassword(email, password)); } catch (err) { handleAuthenticationError(err, setError); } };
Next, find the handleRegistrationAndLogin
function and add the following code
to create a emailPassword
credential.
const handleRegistrationAndLogin = async () => { const isValidEmailAddress = validator.isEmail(email); setError((e) => ({ ...e, password: null })); if (isValidEmailAddress) { try { // Register the user and, if successful, log them in await app.emailPasswordAuth.registerUser(email, password); return await handleLogin(); } catch (err) { handleAuthenticationError(err, setError); } } else { setError((err) => ({ ...err, email: "Email is invalid." })); } };
D. Define the GraphQL Schema & Operations
A GraphQL schema defines all of the types, enums, and scalars that a GraphQL API supports. Realm automatically generates a GraphQL schema for you that includes definitions for your schema types as well as a set of CRUD query and mutation resolvers for each type.
You can use graphql-codegen to generate TypeScript types based on your app's GraphQL schema.
Open src/graphql/useTaskQueries.js
, and find the TODO
for const GetAllTasksQuery
.
Here you define the query to get all tasks from the database.
Fill in the code with the following query:
const GetAllTasksQuery = gql` query GetAllTasksForProject($partition: String!) { tasks(query: { _partition: $partition }) { _id name status } } `;
Now that you've defined the query, you need to create the hook to call it in the application. We're going to define a lightweight wrapper around Apollo's useQuery(). In our hook, we'll pass the project as a dynamic variable to the query. We'll also set the query to refresh every 1000 milliseconds with the Apollo startPolling() function so that the data doesn't get stale if another team member adds tasks to the project. We also must call the stopPolling() function when the component unmounts, so there isn't a memory leak.
Fill in the code:
export function useAllTasksInProject(project) { const { data, loading, error, startPolling, stopPolling } = useQuery(GetAllTasksQuery, { variables: { partition: project.partition }, } ); React.useEffect(() => { // check server for updates every 1000ms startPolling(1000); // stop polling server for data when component unmounts return () => stopPolling(); }, [startPolling, stopPolling]); if (error) { throw new Error(`Failed to fetch tasks: ${error.message}`); } // If the query has finished, return the tasks from the result data // Otherwise, return an empty list const tasks = data?.tasks ?? []; return { tasks, loading }; }
Every poll request counts as a Realm Request for billing purposes. Refer to the Billing documentation for more information.
Using the collection.watch() method as is done in the Implement the Project List section can be more cost effective for queries that receive a high volume of requests.
Now that we've defined the query to fetch tasks by project, we have to define our mutations.
Open src/graphql/useTaskMutations.js
and find the TODO
mutation
definitions. These are all of the mutations that the app uses to create and
modify User
and Task
documents.
Fill in the code with the following mutation definitions:
AddTaskMutation
src/graphql/useTaskMutations.jsconst AddTaskMutation = gql` mutation AddTask($task: TaskInsertInput!) { addedTask: insertOneTask(data: $task) { _id _partition name status } } `; UpdateTaskMutation
src/graphql/useTaskMutations.jsconst UpdateTaskMutation = gql` mutation UpdateTask($taskId: ObjectId!, $updates: TaskUpdateInput!) { updatedTask: updateOneTask(query: { _id: $taskId }, set: $updates) { _id _partition name status } } `; DeleteTaskMutation
src/graphql/useTaskMutations.jsconst DeleteTaskMutation = gql` mutation DeleteTask($taskId: ObjectId!) { deletedTask: deleteOneTask(query: { _id: taskId }) { _id _partition name status } } `;
Now that you've defined the mutations, you need to call them in their respective hooks. The mutation hooks are lightweight wrappers around Apollo's useMutation() hook that allow you to pass dynamic variables to the queries.
In src/graphql/useTaskMutations.js
file, find the following functions below
your mutation definitions and update them to execute their respective mutations:
useAddTask
src/graphql/useTaskMutations.jsfunction useAddTask(project) { const [addTaskMutation] = useMutation(AddTaskMutation, { // Manually save added Tasks into the Apollo cache so that Task queries automatically update // For details, refer to https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates update: (cache, { data: { addedTask } }) => { cache.modify({ fields: { tasks: (existingTasks = []) => [ ...existingTasks, cache.writeFragment({ data: addedTask, fragment: TaskFieldsFragment, }), ], }, }); }, }); const addTask = async (task) => { const { addedTask } = await addTaskMutation({ variables: { task: { _id: new ObjectId(), _partition: project.partition, status: "Open", ...task, }, }, }); return addedTask; }; return addTask; } useUpdateTask
src/graphql/useTaskMutations.jsfunction useUpdateTask(project) { const [updateTaskMutation] = useMutation(UpdateTaskMutation); const updateTask = async (task, updates) => { const { updatedTask } = await updateTaskMutation({ variables: { taskId: task._id, updates }, }); return updatedTask; }; return updateTask; } useDeleteTask
src/graphql/useTaskMutations.jsfunction useDeleteTask(project) { const [deleteTaskMutation] = useMutation(DeleteTaskMutation); const deleteTask = async (task) => { const { deletedTask } = await deleteTaskMutation({ variables: { taskId: task._id }, }); return deletedTask; }; return deleteTask; }
E. Connect Apollo to the GraphQL API
You've defined GraphQL CRUD operations and created custom query mutation hooks
for tasks. However, these hooks must be wrapped in an Apollo context provider
that makes an ApolloClient
object available.
In src/graphql/RealmApolloProvider.js
, we export a React component that
provides the ApolloClient
object but the function that instantiates the
client is incomplete. You need to update the file to create a client that can
connect to your app's GraphQL API.
Instantiate an ApolloClient
The RealmApolloProvider
component should call
createRealmApolloClient()
to instantiate the client. Update the
component with the following code to create an ApolloClient
object that
connects to your app:
export default function RealmApolloProvider({ children }) { const app = useRealmApp(); const [client, setClient] = React.useState(createRealmApolloClient(app)); React.useEffect(() => { setClient(createRealmApolloClient(app)); }, [app]); return <ApolloProvider client={client}>{children}</ApolloProvider>; }
Authenticate GraphQL Requests
The createRealmApolloClient()
function now instantiates a client object, but
you won't be able to run any GraphQL queries or mutations just yet. Every
GraphQL request must include an Authorization header that specifies a valid
user access token. The current client does not include any Authorization
headers, so all requests it makes will fail.
To fix this, update the createRealmApolloClient()
function to include the
current user's access token in an Authorization header with every request:
const createRealmApolloClient = (app) => { const link = new HttpLink({ // Realm apps use a standard GraphQL endpoint, identified by their App ID uri: `https://realm.mongodb.com/api/client/v2.0/app/${app.id}/graphql`, // A custom fetch handler adds the logged in user's access token to GraphQL requests fetch: async (uri, options) => { if (!app.currentUser) { throw new Error(`Must be logged in to use the GraphQL API`); } // Refreshing a user's custom data also refreshes their access token await app.currentUser.refreshCustomData(); // The handler adds a bearer token Authorization header to the otherwise unchanged request options.headers.Authorization = `Bearer ${app.currentUser.accessToken}`; return fetch(uri, options); }, }); const cache = new InMemoryCache(); return new ApolloClient({ link, cache }); };
F. Implement the Projects List
As defined by our data model, a Project
is an embedded object inside
of a User
document. This means that we can use custom user data to access a given user's list of projects
directly from their Realm.User
object.
To update the app if other users add the current user to their project, we use the collection.watch() method.
In the file /graphql/useProjects.js
, add the following code to retrieve the
current user's projects:
function setProjectsFromChange(change, setProjects) { const { fullDocument: { memberOf } } = change; setProjects(memberOf); } export default function useProjects() { const app = useRealmApp(); const [projects, setProjects] = React.useState(app.currentUser.customData.memberOf); if (!app.currentUser) { throw new Error("Cannot list projects if there is no logged in user."); } const mongodb = app.currentUser.mongoClient("mongodb-atlas"); const users = mongodb.db("tracker").collection("User"); // set asynchronous event watcher to react to any changes in the users collection React.useEffect(() => { let changeWatcher; (async () => { changeWatcher = users.watch(); for await (const change of changeWatcher) { setProjectsFromChange(change, setProjects); } })(); // close connection when component unmounts return () => changeWatcher.return() }); return projects; }
G. Use Realm functions
The file src/components/EditPermissionsModal.js
defines a hook named
useTeamMembers()
that returns a list of the current user's team members. It
should also return functions that add and remove team members, but you'll need
to define them. The task tracker backend application already defines server-side
Realm functions that handle the logic, so you
just need to call them.
You can call your app's Realm functions by name as asynchronous
methods on the User.functions
object. All function calls return a
Promise
. For example, to call a function named myFunction
as the
current user, you would write await app.currentUser.functions.myFunction()
.
Update the useTeamMembers()
hook's return object to include
addTeamMember()
and removeTeamMember()
helper functions that accept an
email address and pass it their respective server-side functions:
function useTeamMembers() { const [teamMembers, setTeamMembers] = React.useState(null); const [newUserEmailError, setNewUserEmailError] = React.useState(null); const app = useRealmApp(); const { addTeamMember, removeTeamMember, getMyTeamMembers } = app.currentUser.functions; const updateTeamMembers = () => { getMyTeamMembers().then(setTeamMembers); }; // display team members on load React.useEffect(updateTeamMembers, []); return { teamMembers, errorMessage: newUserEmailError, addTeamMember: async (email) => { const { error } = await addTeamMember(email); if (error) { setNewUserEmailError(error); return { error }; } else { updateTeamMembers(); } }, removeTeamMember: async (email) => { await removeTeamMember(email); updateTeamMembers(); }, }; }
H. Try It Out Locally
The task tracker app is now fully configured so you can start it up to start tracking tasks.
Start the App
To start the app, navigate to the project root in your shell and then enter the following command:
npm run start
If the app starts successfully, you should see output that resembles the following:
Compiled successfully! You can now view task-tracker in the browser. Local: http://localhost:3000 On Your Network: http://191.175.1.124:3000
Open your browser to http://localhost:3000 to access the app.
If the app builds successfully, here are some things you can try in the app:
- Create a user with email first@example.com
- Explore the app, then log out or launch a second instance of the app in an incognito browser window
- Create another user with email second@example.com
- Navigate to second@example.com's project
- Add, update, and remove some tasks
- Click "Manage Team"
- Add first@example.com to your team
- Log out and log in as first@example.com
- See two projects in the projects list
- Navigate to second@example.com's project
- Collaborate by adding, updating, and removing some new tasks
I. Deploy the App
Now that you've validated that the app is working, it's time to deploy the app. In this section, you're going to create a production build with Create React App's build script and deploy the app to be hosted with Realm Static Hosting.
Create the Production Bundle
To create your production bundle, run the command:
npm run build
Once the build finishes, validate it's working by running:
npx serve -s build
You will see a new browser window pop up running the app on localhost:5000
. The app should
look and behave the same as the development version you made in the previous step.
You can find the production bundle's code with your repository in a newly generated folder build
.
Deploy Using the Realm UI
First, navigate to the Hosting section of the Realm UI. You can find it on the side menu under Manage. Once you're on the Hosting page, click the Enable Hosting button.
Now that you have hosting enabled, replace the default index.html
with the production
bundle you built in the last step. Delete the index.html
file by clicking the check
box next to the file and then Actions > Delete.
To add your app, open a file browser on your computer, and navigate to the build
folder in your app's root folder. Select all the build
folder's contents, and
drag and drop it over to the Realm Hosting UI's Files page.
Once the files finish uploading, click the Review & Deploy button at the top of the page. A dialog will open with your changes. Review the changes and click the Deploy button at the bottom of the dialogue to start the deployment of your app.
Realm deploys your site to <Your-App-Id>.mongodbstitch.com
.
Deployment can take up to a few minutes. You can track the status on the Hosting page.
Once deployment finishes, navigate to your app's new URL, <Your-App-Id>.mongodbstitch.com
.
Here you'll find the app with the same backend and data that you were working with
before, now live on the Internet!
For more detailed information about hosting your web app with Realm, refer to our documentation on hosting a single page application.
You can also deploy the app frontend with the Realm CLI. Before you're able to deploy the frontend from the Realm CLI, you must first enable hosting in the Realm UI, as discussed at the beginning of the Deploy Using the Realm UI section.
Once you've enabled hosting in the UI, you need to have the realm-tutorial-backend repository handy, which you used in the backend setup.
In the backend directory, create the folder hosting/files
. This is where
you will place your assets for Realm Hosting.
Also create a config file for the app. This should be located at hosting/config.json
,
and have the following contents:
{ "enabled": true, "default_response_code": 200, "default_error_path": "/index.html", }
Copy the contents of the build
folder from the realm-tutorial-web directory
to the hosting/files
folder in the backend. You can do this with the command:
cp -a path/to/realm-tutorial-web/build/. path/to/realm-tutorial-backend/hosting/files
Now you're ready to deploy the app using the Realm CLI. To deploy, run the command:
realm-cli push --remote=<Your-App-Id> --include-hosting
Realm then deploys your site to <Your-App-Id>.mongodbstitch.com
.
What's Next?
You just built and deployed a functional task tracker web application built with MongoDB Realm. Great job!
Now that you have some hands-on experience with MongoDB Realm, consider these options to keep practicing and learn more:
Extend the task tracker app with additional features. For example, you could:
- allow users to log in using another authentication provider
Follow another tutorial to build a mobile app for the task tracker. We have task tracker tutorials for the following platforms:
- Use the Realm CLI to automate deploying your frontend application.
Dive deeper into the docs to learn more about MongoDB Realm. You'll find information and guides on features like:
- Serverless functions that handle backend logic and connect your app to external services. You can call functions from a client app, either directly or as a custom GraphQL resolver.
- Triggers and HTTPS endpoints, which automatically call functions in response to events as they occur. You can define database triggers which respond to changes in your data, authentication triggers which respond to user management and authentication events, and scheduled triggers which run on a fixed schedule.
- Built-in authentication providers and and user management tools. You can allow users to log in through multiple methods, like API keys and Google OAuth, and associate custom data with every user.
How did it go? Use the Give Feedback tab at the bottom right of the page to let us know if this tutorial was helpful or if you had any issues.