HomeLearnHow-toQuerying MongoDB in the Browser with React and Realm

Querying MongoDB in the Browser with React and Realm

Published: Jan 28, 2021

  • MongoDB
  • Atlas
  • Realm
  • ...

By Aaron Bassett

Rate this article

When we think of connecting to a database, we think of serverside. Our application server connects to our database server using the applicable driver for our chosen language. But with the MongoDB Realm Web SDK, we can run queries against our Atlas cluster from our web browser, no server required.

#Security and Authentication

One of the primary reasons database connections are traditionally server-to-server is so that we do not expose any admin credentials. Leaking credentials like this is not a concern with Realm as it has APIs for user management, authentication, and access control. There are no admin credentials to expose as each user has a separate account. Then, using Rules, we control what data each user has permission to access.

The MongoDB service uses a strict rules system that prevents all operations unless they are specifically allowed. MongoDB Realm determines if each operation is allowed when it receives the request from the client, based on roles that you define. Roles are sets of document-level and field-level CRUD permissions and are chosen individually for each document associated with a query.

Rules have the added benefit of enforcing permissions at the data access level, so you don't need to include any permission checks in your application logic.

#Creating an Atlas Cluster and Realm App

You can find instructions for creating a free MongoDB Atlas cluster and Realm App in our documentation: Create a Realm App (Realm UI).

We're going to be using one of the sample datasets in this tutorial, so after creating your free cluster, click on Collections and select the option to load a sample dataset. Once the data is loaded, you should see several new databases in your cluster. We're going to be using the sample_mflix database in our code later.

#Users and Authentication Providers

Realm supports a multitude of different authentication providers, including Google, Apple, and Facebook. For this tutorial, we're going to stick with regular email and password authentication.

In your Realm App, go to the Users section and enable the Email/Password provider, the user confirmation method should be "automatic", and the password reset method should be a reset function. You can use the provided stubbed reset function for now.

In a real-world application, we would have a registration flow so that users could create accounts. But for the sake of this tutorial, we're going to create a new user manually. While still in the "Users" section of your Realm App, click on "Add New User" and enter the email and password you would like to use.

#Rules and Roles

Rules and Roles govern what operations a user can perform. If an operation has not been explicitly allowed, Realm will reject it. At the moment, we have no Rules or Roles, so our users can't access anything. We need to configure our first set of permissions.

Navigate to the "Rules" section and select the sample_mflix database and the movies collection. Realm has several "Permissions Template"s ready for you to use.

  • Users can only read and write their own data.
  • Users can read all data, but only write their own data.
  • Users can only read all data.

These are just the most common types of permissions; you can create your own much more advanced rules to match your requirements.

  • Configure a role that can only insert documents.
  • Define field-level read or write permissions for a field in an embedded document.
  • Determine field-level write permissions dynamically using a JSON expression.
  • Invoke a Realm Function to perform more involved checks, such as checking data from a different collection.

Read the documentation "Configure Advanced Rules" for more information.

We only want our users to be able to access their data and nothing else, so select the "Users can only read and write their own data" template.

Realm does not stipulate what field name you must use to store your user id; we must enter it when creating our configuration. Enter authorId as the field name in this example.

By now, you should have the Email/Password provider enabled, a new user created, and rules configured to allow users to access any data they own. Ensure you deploy all your changes, and we can move onto the code.

#Creating Our Web Application with React

Download the source for our demo application from GitHub.

Once you have the code downloaded, you will need to install a couple of dependencies.

1npm install

#The Realm App Provider

As we're going to require access to our Realm App client throughout our React component tree, we use a Context Provider. You can find the context providers for this project in the providers folder in the repo.

1import * as RealmWeb from "realm-web"
2
3import React, { useContext, useState } from "react"
4
5const RealmAppContext = React.createContext(null)
6
7const RealmApp = ({ children }) => {
8 const REALM_APP_ID = "realm-web-demo"
9 const app = new RealmWeb.App({ id: REALM_APP_ID })
10 const [user, setUser] = useState(null)
11
12 const logIn = async (email, password) => {
13 const credentials = RealmWeb.Credentials.emailPassword(email, password)
14 try {
15 await app.logIn(credentials)
16 setUser(app.currentUser)
17 return app.currentUser
18 } catch (e) {
19 setUser(null)
20 return null
21 }
22 }
23
24 const logOut = () => {
25 if (user !== null) {
26 app.currentUser.logOut()
27 setUser(null)
28 }
29 }
30
31 return (
32 <RealmAppContext.Provider value={{ logIn, logOut, user, }} > {children} </RealmAppContext.Provider>
33 )
34}
35
36export const useRealmApp = () => {
37 const realmContext = useContext(RealmAppContext)
38 if (realmContext == null) {
39 throw new Error("useRealmApp() called outside of a RealmApp?")
40 }
41 return realmContext
42}
43
44export default RealmApp

This provider handles the creation of our Realm Web App client, as well as providing methods for logging in and out. Let's look at these parts in more detail.

1const RealmApp = ({ children }) => {
2 const REALM_APP_ID = "realm-web-demo"
3 const app = new RealmWeb.App({ id: REALM_APP_ID })
4 const [user, setUser] = useState(null)

The value for REALM_APP_ID is on your Realm App dashboard. We instantiate a new Realm Web App with the relevant ID. It is this App which allows us to access the different Realm services. You can find all required environment variables in the .envrc.example file.

You should ensure these variables are available in your environment in whatever manner you normally use. My personal preference is direnv.

1const logIn = async (email, password) => {
2 const credentials = RealmWeb.Credentials.emailPassword(email, password)
3 try {
4 await app.logIn(credentials)
5 setUser(app.currentUser)
6 return app.currentUser
7 } catch (e) {
8 setUser(null)
9 return null
10 }
11}

The logIn method accepts the email and password provided by the user and creates a Realm credentials object. We then use this to attempt to authenticate with our Realm App. If successful, we store the authenticated user in our state.

#The MongoDB Provider

Just like the Realm App context provider, we're going to be accessing the Atlas service throughout our component tree, so we create a second context provider for our database.

1import React, { useContext, useEffect, useState } from "react"
2
3import { useRealmApp } from "./realm"
4
5const MongoDBContext = React.createContext(null)
6
7const MongoDB = ({ children }) => {
8 const { user } = useRealmApp()
9 const [db, setDb] = useState(null)
10
11 useEffect(() => {
12 if (user !== null) {
13 const realmService = user.mongoClient("mongodb-atlas")
14 setDb(realmService.db("sample_mflix"))
15 }
16 }, [user])
17
18 return (
19 <MongoDBContext.Provider value={{ db, }} > {children} </MongoDBContext.Provider>
20 )
21}
22
23export const useMongoDB = () => {
24 const mdbContext = useContext(MongoDBContext)
25 if (mdbContext == null) {
26 throw new Error("useMongoDB() called outside of a MongoDB?")
27 }
28 return mdbContext
29}
30
31export default MongoDB

The Realm Web SDK provides us with access to some of the different Realm Services, as well as our custom functions. For this example, we are only interested in the mongodb-atlas service as it provides us with access to the linked MongoDB Atlas cluster.

1useEffect(() => {
2 if (user !== null) {
3 const realmService = user.mongoClient("mongodb-atlas")
4 setDb(realmService.db("sample_mflix"))
5 }
6}, [user])

In this React hook, whenever our user variable updates—and is not null, so we have an authenticated user—we set our db variable equal to the database service for the sample_mflix database.

Once the service is ready, we can begin to run queries against our MongoDB database in much the same way as we would with the Node.js driver.

However, it is a subset of actions, so not all are available—the most notable absence is collection.watch(), but that is being actively worked on and should be released soon—but the common CRUD actions will work.

#Wrap the App in Index.js

The default boilerplate generated by create-react-app places the DOM renderer in index.js, so this is a good place for us to ensure that we wrap the entire component tree within our RealmApp and MongoDB contexts.

1ReactDOM.render(
2 <React.StrictMode> <RealmApp> <MongoDB> <App /> </MongoDB> </RealmApp> </React.StrictMode>,
3 document.getElementById("root")
4)

The order of these components is essential. We must create our Realm Web App first before we attempt to access the mongodb-atlas service. So, you must ensure that <RealmApp> is before <MongoDB>. Now that we have our <App /> component nestled within our Realm App and MongoDB contexts, we can query our Atlas cluster from within our React component!

#The Demo Application

Our demo has two main components: a login form and a table of movies, both of which are contained within the App.js. Which component we show depends upon whether the current user has authenticated or not.

Screenshot of login form
1function LogInForm(props) {
2 return (
3 <Pane alignItems="center" justifyContent="center" display="flex" paddingTop={50}> <Pane width="50%" padding={16} background="purpleTint" borderRadius={3} elevation={4}> <Heading size={800} marginTop="10" marginBottom="10"> Log in </Heading> <Pane> <TextInputField label="Username" required placeholder="mongodb@example.com" onChange={(e) => props.setEmail(e.target.value)} value={props.email} /> </Pane> <Pane> <TextInputField label="Password" required placeholder="**********" type="password" onChange={(e) => props.setPassword(e.target.value)} value={props.password} /> </Pane> <Button appearance="primary" onClick={props.handleLogIn}> Log in </Button> </Pane> </Pane>
4 )
5}

The login form consists of two controlled text inputs and a button to trigger the handleLogIn function.

Screenshot of movies table
1function MovieList(props) {
2 return (
3 <Pane alignItems="center" justifyContent="center" display="flex" paddingTop={50}> <Pane width="50%" padding={16} background="purpleTint" borderRadius={3} elevation={4}> <Table> <Table.Head> <Table.TextHeaderCell>Title</Table.TextHeaderCell> <Table.TextHeaderCell>Plot</Table.TextHeaderCell> <Table.TextHeaderCell>Rating</Table.TextHeaderCell> <Table.TextHeaderCell>Year</Table.TextHeaderCell> </Table.Head> <Table.Body height={240}> {props.movies.map((movie) => ( <Table.Row key={movie._id}> <Table.TextCell>{movie.title}</Table.TextCell> <Table.TextCell>{movie.plot}</Table.TextCell> <Table.TextCell>{movie.rated}</Table.TextCell> <Table.TextCell isNumber>{movie.year}</Table.TextCell> </Table.Row> ))} </Table.Body> </Table> <Button height={50} marginRight={16} appearance="primary" intent="danger" onClick={props.logOut} > Log Out </Button> </Pane> </Pane>
4 )
5}

The MovieList component renders an HTML table with a few details about each movie, and a button to allow the user to log out.

1function App() {
2 const { logIn, logOut, user } = useRealmApp()
3 const { db } = useMongoDB()
4 const [email, setEmail] = useState("")
5 const [password, setPassword] = useState("")
6 const [movies, setMovies] = useState([])
7
8 useEffect(() => {
9 async function wrapMovieQuery() {
10 if (user && db) {
11 const authoredMovies = await db.collection("movies").find()
12 setMovies(authoredMovies)
13 }
14 }
15 wrapMovieQuery()
16 }, [user, db])
17
18 async function handleLogIn() {
19 await logIn(email, password)
20 }
21
22 return user && db && user.state === "active" ? (
23 <MovieList movies={movies} user={user} logOut={logOut} />
24 ) : (
25 <LogInForm email={email} setEmail={setEmail} password={password} setPassword={setPassword} handleLogIn={handleLogIn} />
26 )
27}
28
29export default App

Here, we have our main <App /> component. Let's look at the different sections in order.

1const { logIn, logOut, user } = useRealmApp()
2const { db } = useMongoDB()
3const [email, setEmail] = useState("")
4const [password, setPassword] = useState("")
5const [movies, setMovies] = useState([])

We're going to use the Realm App and the MongoDB provider in this component: Realm App for authentication, MongoDB to run our query. We also set up some state to store our email and password for logging in, and hopefully later, any movie data associated with our account.

1useEffect(() => {
2 async function wrapMovieQuery() {
3 if (user && db) {
4 const authoredMovies = await db.collection("movies").find()
5 setMovies(authoredMovies)
6 }
7 }
8 wrapMovieQuery()
9}, [user, db])

This React hook runs whenever our user or db updates, which occurs whenever we successfully log in or out. When the user logs in—i.e., we have a valid user and a reference to the mongodb-atlas service—then we run a find on the movies collection.

1const authoredMovies = await db.collection("movies").find()

Notice we do not need to specify the User Id to filter by in this query. Because of the rules, we configured earlier only those documents owned by the current user will be returned without any additional filtering on our part.

#Taking Ownership of Documents

If you run the demo and log in now, the movie table will be empty. We're using the sample dataset, and none of the documents within it belongs to our current user. Before trying the demo, modify a few documents in the movies collection and add a new field, authorId, with a value equal to your user's ID. You can find their ID in the Realm App Users section.

Screenshot of Realm Users list

Once you have given ownership of some documents to your current user, try running the demo application and logging in.

Screen recording of demo app showing log in and the movies table populating

Congratulations! You have successfully queried your database from within your browser, no server required!

#Change the Rules

Try modifying the rules and roles you created to see how it impacts the demo application.

Screen recording of deleting rules in the Realm App dashboard

Ignore the warning and delete the configuration for the movies collection. Now, your App should die with a 403 error: "no rule exists for namespace' sample_mflix.movies.'"

Use the "Users can read all data, but only write their own data" template. I would suggest also modifying the find() or adding a limit() as otherwise, the demo will try to show every movie in your table!

Add field-level permissions. In this example, non-owners cannot write to any documents, but they can read the title and year fields for all documents.

1{
2 "roles": [
3 {
4 "name": "owner",
5 "apply_when": {
6 "authorId": "%%user.id"
7 },
8 "insert": true,
9 "delete": true,
10 "search": true,
11 "read": true,
12 "write": true,
13 "fields": {
14 "title": {},
15 "year": {}
16 },
17 "additional_fields": {}
18 },
19 {
20 "name": "non-owner",
21 "apply_when": {},
22 "insert": false,
23 "delete": false,
24 "search": true,
25 "write": false,
26 "fields": {
27 "title": {
28 "read": true
29 },
30 "year": {
31 "read": true
32 }
33 },
34 "additional_fields": {}
35 }
36 ],
37 "filters": [
38 {
39 "name": "filter 1",
40 "query": {},
41 "apply_when": {},
42 "projection": {}
43 }
44 ],
45 "schema": {}
46}

#Further Reading

For more information on MongoDB Realm and the Realm Web SDK, I recommend reading our documentation:

If you haven't yet set up your free cluster on MongoDB Atlas, now is a great time to do so. You have all the instructions in this blog post.

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

Rate this article
MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.