Navigation

Task Tracker (Web)

Note
Check Out the Quick Start

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.

Important
Realm Web SDK and Sync

The Realm Web SDK does not support sync, so the app you'll build in this tutorial won't update in real time when data changes. 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.

Before you get started, you'll need the following:

Once you're set up with these prerequisites, you're ready to start the tutorial.

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
Tip

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.

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
└── useTasks.js

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.

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.

Note
React Components

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.

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.

1

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:

``src/App.js``
export const APP_ID = "<your Realm app ID here>";
Note
Use Your App ID

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.

The copy app id button in the UI
2

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.

``src/RealmApp.js``
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>
);
};
Example
How We Use It

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.

``src/App.js``
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." }));
}
};

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.

Tip
Generated TypeScript Types

You can use graphql-codegen to generate TypeScript types based on your app's GraphQL schema.

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

    const AddTaskMutation = gql` mutation AddTask($task: TaskInsertInput!) { addedTask: insertOneTask(data: $task) { _id _partition name status } } `;
  • UpdateTaskMutation

    const UpdateTaskMutation = gql` mutation UpdateTask($taskId: ObjectId!, $updates: TaskUpdateInput!) { updatedTask: updateOneTask(query: { _id: $taskId }, set: $updates) { _id _partition name status } } `;
  • DeleteTaskMutation

    const 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

    function 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

    function 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

    function useDeleteTask(project) {
    const [deleteTaskMutation] = useMutation(DeleteTaskMutation);
    const deleteTask = async (task) => {
    const { deletedTask } = await deleteTaskMutation({
    variables: { taskId: task._id },
    });
    return deletedTask;
    };
    return deleteTask;
    }

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.

1

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:

``src/graphql/RealmApolloProvider.js``
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>;
}
2

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:

``src/graphql/RealmApolloProvider.js``
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 });
};

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.

In the file /graphql/useProjects.js, add the following code to retrieve the current user's projects:

export default function useProjects() {
const app = useRealmApp();
if (!app.currentUser) {
throw new Error("Cannot list projects if there is no logged in user.");
}
const projects = app.currentUser.customData.memberOf;
return projects;
}

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.

Tip

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.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();
},
};
}

The task tracker app is now fully configured so you can start it up to start tracking tasks.

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

You just built 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:

Note
Leave Feedback

How did it go? Please let us know if this tutorial was helpful or if you had any issues by using the feedback widget on the right side of the page.

Give Feedback