Navigation

Task Tracker (Web)

Info With Circle IconCreated with Sketch.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 With Circle IconCreated with Sketch.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 client application repository directly from GitHub:

git clone 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 realm-tutorial-react-native repository contains two branches: final and start. The final branch is a finished version of the app as it should look after you complete this tutorial. To walk through this tutorial, please check out the start branch:

git checkout start

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/
│ ├── 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/realm directory contains all of the modules that you'll use to connect the application to Realm. 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 and /src/hooks directories contain pre-built React components and hooks that handle local state management and UI rendering. The components import code from the files in /src/realm and use them to interact with Realm. We've already completely implemented these so you won't need to add any code to these files. We'll make sure to show you along the way how these components and hooks use the code that you write.

Info With Circle IconCreated with Sketch.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>";
Info With Circle IconCreated with Sketch.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>
);
};
Beaker IconExample
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, and you can use it to generate useful TypeScript types for your data.

In addition to generating types from a schema file, you can also generate functions and GraphQL objects for operations such as queries and mutations. You can split operations across multiple files, but in this tutorial we'll define all of the CRUD operations that the app uses to work with tasks and user documents in a single file.

Navigate to the src/graphql/useTaskMutations.js file and define the following mutations:

  • 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 } } `;

The custom query and mutation hooks are lightweight wrappers around Apollo's useQuery() and useMutation() hooks. Once you have completed defining the task mutations, you will need to implement functions that execute those mutations. In the same src/graphql/useTaskMutations.js file, find the following functions and call the appropriate 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;
    }

We've defined GraphQL CRUD operations and created custom query and 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, projects are an embedded object of the users object. Therefore, if we want to retrieve a list of projects, we'll have to access the user custom data object.

Info With Circle IconCreated with Sketch.Note

To learn more about how Realm Object models are used in Node.js applications, see Realm Objects in our Node.js client guide.

In the file /graphql/useProjects.js, we need to implement the retrieval of the current user's projects. We'll need to add the following code:

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;
}

In the EditPermissionsModal.js file, there are functions that rely on Realm functions. Realm functions allow you to execute server-side logic for your client applications.

First, you need to import the functions for client-side usage. Next, you will need to implement the function updateTeamMembers() so that it calls the getMyTeamMembers function and updates the team variable with the current team members. Then you need to implement the addTeamMember() and removeTeamMember() functions so that it adds/removes the user as a team member when provided with their email.

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:

Info With Circle IconCreated with Sketch.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