Building a To-Do List React App with Dgraph - Dgraph Blog

Before We Begin

In this tutorial we will build a To-Do List application using React JavaScript library and Dgraph as a backend database. We will use dgraph-js-http to greatly simplify the life of JavaScript developers when accessing Dgraph databases.

The tutorial is split into several steps:

  1. Creating a React App

  2. Starting a local Dgraph server using docker-compose

  3. Connecting to Dgraph from JavaScript and fetching data

  4. Storing data in Dgraph

If you are already familiar with setting up a React application and want to skip straight to the Dgraph-specific part of the tutorial, you can start from Section 2.

1. Creating a React App

Nowadays itā€™s pretty simple to get started with React, thanks to the awesome create-react-app script. Letā€™s run it:

  npx create-react-app dgraph-react-todomvc

(npx command is part a of the standard Node.js installation.)

Verifying the new app

Letā€™s compile and test our application. Like most tasks with create-react-app we need only one command to run the development server and it will even open a browser window for us:

  yarn start

![](upload://jDubfNgYPKZzbdL8LwNgxl59X0J.jpeg)

Once weā€™ve made sure everything is working as expected itā€™s a good time to save our progress so far to a version control system. At Dgraph weā€™re using GitHub so I will just commit everything with Git:

 git commit -am 'Output of the create-react-app'

And since Iā€™ve created a public repository, you can view and clone all steps of this tutorial at any time. (I wonā€™t digress on how I did this, as it goes beyond the scope of this tutorial.)

Adding custom code

So far weā€™ve been generating boilerplate code not specific to the to-do application weā€™re building. Itā€™s time to make our app more unique, but as the first step, letā€™s just delete all the styling weā€™re not going to need.

As this is not a very creative process (quite the opposite of creative, actually) I wonā€™t go into details. You can view the changed files on GitHub.

Connecting TodoMVC styles

Since Iā€™m not a designer, but still like my apps to look pretty, Iā€™m going to use high quality off-the-shelf CSS styles, released by the TodoMVC team.

Adding those to the project is as easy as

 yarn add todomvc-app-css

We also need to load the new styles onto the page, by importing them in the App.js:

  import 'todomvc-app-css/index.css'

And after these two changes we can write some HTML to use the styles. Letā€™s update our main render() method to do just that (also in the App.js):

  ...
  render() {
    return (
      <div>
        <header className="header">
          <h1>todos</h1>
          <input
            className="new-todo"
            placeholder="What needs to be done?"
            autoFocus={true}
          />
        </header>
      </div>
    )
  }
  ...

Now, since we still have our dev server running in the background (the one we started with yarn start), our app will get live reloaded and start to look pretty fancy:

![](upload://ixKjtUEqZX7z68nlaJKylSCghYg.jpeg)

Sadly it isnā€™t doing anything useful but weā€™ll get to that in a minute.

Iā€™ve also made small tweaks to the index.html but those are fairly self-explanatory, and can be seen in the git history.

Adding React Components

Thereā€™s just one more step before we get to the juicy bit - using Dgraph. Trust me, I want to start telling you about it ASAP.

So, without further ado, letā€™s get our frontend code done with by copying the ready made React components from the TodoMVC React Example.

The files Iā€™ve added are somewhat different to the ā€œofficialā€ TodoMVC React code because Iā€™ve modified the source to use all the shiny new features of modern JavaScript, Babel and Webpack: classes, modules, lambda functions, let/const keywords etc.

Itā€™s another hairy but straightforward refactoring, so letā€™s not spend too much time on it.

After the dust settled our app can respond to the user input, show and store some to-dos:

![](upload://7ojivYtliLClxdykZIMjyvQ6bYU.jpeg)

2. Starting a local Dgraph server

There are many ways to install and run Dgraph depending on your machine setup. I personally prefer to use docker-compose as it keeps all of the configuration in one file.

First, letā€™s create a docker-compose.yml configuration file. Itā€™s up to you where you want to keep it, for our project the folder dgraph-react-todomvc/dgraph/ seems logical.

version: "3.2"
services:
  zero:
    image: dgraph/dgraph:v1.0.13
    volumes:
      - type: volume
        source: dgraph
        target: /dgraph
        volume:
          nocopy: true
    ports:
      - 5080:5080
      - 6080:6080
    restart: on-failure
    command: dgraph zero --my=zero:5080
  server:
    image: dgraph/dgraph:v1.0.13
    volumes:
      - type: volume
        source: dgraph
        target: /dgraph
        volume:
          nocopy: true
    ports:
      - 8080:8080
      - 9080:9080
    restart: on-failure
    command: dgraph alpha --my=server:7080 --lru_mb=2048 --zero=zero:5080
  ratel:
    image: dgraph/dgraph:v1.0.13
    volumes:
      - type: volume
        source: dgraph
        target: /dgraph
        volume:
          nocopy: true
    ports:
      - 8000:8000
    command: dgraph-ratel
volumes:
  dgraph:

Then we can fire up all three processes (Dgraph Alpha, Zero and Ratel) with one simple command:

  docker-compose up

I usually run it in a separate terminal so I can check the server output at any time or shutdown everything by pressing Ctrl+C.

The output of docker-compose will be a bit noisy, but should look something like this:

Viewing database with Ratel

It sure looks like itā€™s doing something how do we know if it works as expected? Letā€™s fire up a web browser and give Ratel a try ā€” Dgraphā€™s web UI.

Opening http://localhost:8000 should take you to the Ratel loading screen.

From there you can click on šŸš€ Launch Latest to load the latest stable version of Ratel and run some queries:

![](upload://5tkq0JHJjPLL9nD111OyUXNKDF2.gif)

3. Connecting to Dgraph from JavaScript and fetching data

Dgraph team has built client libraries for various languages. Since we are building a web app without a backend server, and JavaScript in the browser is very restricted in what it is allowed to do, we will be using dgraph-js-http.

You may have noticed thereā€™s also dgraph-js available via npm.

The main difference between the two is dgraph-js-http communicates with Dgraph via HTTP queries and dgraph-js uses network sockets and gRPC. gRPC is more efficient, but currently thereā€™s no network sockets API available to webpages. Therefore dgraph-js-http is our only option. For many use cases, the difference in performance is negligible so letā€™s start building our application around the HTTP protocol.

Installing dgraph-js-http

As with any other npm package, adding dgraph-js-http to our project is super simple:

  yarn install dgraph-js-http

And after yarn has downloaded the latest version of the package, we can import it in our TodoModel.js:

import * as dgraph from 'dgraph-js-http'

In order to communicate with Dgraph we need to create an instance of the DgraphClient. Letā€™s do it inside our TodoModel constructor:

  const clientStub = new dgraph.DgraphClientStub("http://localhost:8080")
  this.dgraph = new dgraph.DgraphClient(clientStub)

With a client object ready we can fetch to-dos using a GraphQL+- query:

  async fetchTodos() {
    const query = `{
      todos(func: has(is_todo))
      {
        uid
        title
        completed
      }
    }`
    const res = await this.dgraph.newTxn().query(query)
    return res.data.todos || []
  }

We also need to call fetchTodos() when our web app is loaded, store its result inside the model, and also get rid of the no-longer-needed localStorage code.

For the former task Iā€™ve created a helper method fetchAndInform():

  async fetchAndInform() {
    this.todos = await this.fetchTodos()
    this.inform()
  }

And placed it as the last call in the TodoModel constructor:

  constructor() {
    const clientStub = new dgraph.DgraphClientStub("http://localhost:8080")
    this.dgraph = new dgraph.DgraphClient(clientStub)
    this.todos = []
    this.fetchAndInform()
  }

As the final touch, we can stop manually generating unique ids and start using much more compact and efficient Dgraphā€™s uid field:

![](upload://xkIwzu7ei2q6pUluYS4wde30gcg.svg)

All the major changes Iā€™ve outlined above and some minor cleanups Iā€™ve omitted are available in this GitHub commit.

Adding test data to Dgraph

So weā€™ve written a bit of code, removed a bit more, but how do we know if itā€™s actually working as expected?

If we simply reload the web app we will see an empty to-do list. Any items we add via the UI disappear on browser refresh.

That is happening because our Dgraph database is empty at the moment and we havenā€™t coded our components to populate it yet.

Every time the page is refreshed our query returns an empty set of to-dos, and that is what we see on the webpage.

Since we have the Ratel UI (we have already played with it) we can easily add some test data.

Letā€™s open our local Ratel instance (http://localhost:8000/?latest) and execute the following mutation:

{
  "set": [
    {
      "uid": "_:todo1",
      "is_todo": "true",
      "title": "First Todo",
      "completed": "false"
    },
    {
      "uid": "_:todo2",
      "is_todo": "true",
      "title": "Second Todo",
      "completed": "true"
    }
  ]
}

In Ratel the successful response should look like this:

![](upload://al3o3ZSq3Wl3AsvBfyHeC6kLuhU.jpeg)

Note that we needed to click the ā€œMutateā€ radio button.

Ratel also lets us test the query our application is sending:

![](upload://u8gMxHunoGrafN8MziBEFGSrApm.jpeg)

Now, after we made sure we have the right data in the database, letā€™s see if our application is accessing it correctly:

![](upload://opeTyVcE5M9JFe2yTrt69QTbFRz.jpeg)

This is almost perfect, except our mutation said only the second to-do was completed, but the first was not. Why did this happen?

If you take a closer look at the Ratel query screenshot above, you will notice that completed is returned as a JSON string, not a boolean:

  ...
      {
        "uid": "0x3",
        "title": "First Todo",
        "completed": "false"
      },
  ...

That second pair of double quotes does matter in the line "completed": "false". We have not yet told Dgraph to parse the values of completed as booleans, so Dgraph is storing and returning them as strings. To change that we will need to adjust Schema.

Modifying Schema with Ratel

As we have determined, we need to tell Dgraph to convert the completed predicate from string to boolean.

Ratel has full support for managing Dgraph schemas, so such changes takes only a handful of clicks:

![](upload://nMnCLsZoB4yzBtQVoLVSQzALmNc.jpeg)

After weā€™ve modified the schema Dgraph takes care of the rest ā€” it will migrate existing data to the new type and will start responding to new queries with values coerced to the right data type.

We can verify that this is actually happening by re-running the query in Ratel, or by refreshing our TodoMVC app:

4. Storing data in Dgraph

We have learned how to query Dgraph for data and show results to the user. The only missing piece of the puzzle is writing our to-dos to Dgraph so they donā€™t get lost every time our users close a browser tab.

Letā€™s start by sending newly created to-dos to Dgraph, and then weā€™ll write code for updating or deleting the existing ones.

Creating new to-do items in Dgraph

We represent to-do items as graph nodes. To create a new node weā€™ll need a transaction, and a mutation:

  async addTodo(title) {
    try {
      const res = await this.dgraph.newTxn().mutate({
        setJson: {
          uid: "_:newTodo",
          is_todo: true,
          title,
          completed: false,
        },
        commitNow: true,
      })
      console.info('Created new to-do with uid', res.data.uids.newTodo)
    } catch (error) {
      alert('Database write failed!')
      console.error('Network error', error)
    } finally {
      this.fetchAndInform()
    }
  }

commitNow informs Dgraph that this transaction will not modify any more data and should be committed right away.

In a more complex application we could set it to false (or omit it altogether) if we planned to perform more reads or mutations. In that case weā€™d have to manually call commit() at the right moment.

Note how the response object contains the uid of the newly created item. Since we used the alias "_:newTodo" in our setJson mutation, the uid of that node is stored in the uids.newTodo of the response data.

Our application can now write data to Dgraph, and we can see auto-incremented uid values printed to console:

![](upload://aOEG4eMvJ3LY4U0ntZ8mBANBCx4.jpeg)

You can also go to Ratel and re-run our query ā€” the new to-dos will appear in the response.

Deleting nodes from Dgraph

Code to delete a to-do is very similar to that for creating a to-do. We only need to specify a uid of the node being deleted, and use the deleteJson field on the mutation object:

  async destroy(todo) {
    try {
      await this.dgraph.newTxn().mutate({
        deleteJson: {
          uid: todo.uid
        },
        commitNow: true,
      })
    } catch (error) {
      alert('Database write failed!')
      console.error('Network error', error)
    } finally {
      this.fetchAndInform()
    }
  }

Deleting multiple to-dos for the clearCompleted() method can be done by passing an array in the deleteJson:

    ...
    const uidsToDelete = this.todos
        .filter(({ completed }) => completed)
        .map(({ uid }) => ({ uid }))
    await this.dgraph.newTxn().mutate({
      deleteJson: uidsToDelete,
      commitNow: true,
    })
    ...
Updating data in Dgraph

Our application can create new to-dos in the database, can read the data back, and can remove unnecessary data.

We just need to enable our users to edit their existing to-dos and weā€™re done!

As you may have guessed already, editing a node property is just another transaction:

  async save(todoToSave, newTitle) {
    try {
      await this.dgraph.newTxn().mutate({
        setJson: {
          uid: todoToSave.uid,
          title: newTitle,
        },
        commitNow: true,
      })
    } catch (error) {
      console.error('Network error', error)
    } finally {
      this.fetchAndInform()
    }
  }

We donā€™t need to pass the entire object back to Dgraph. We only include the new value for title in our mutation. Other predicates (such as completed) will have their values preserved.

Methods that change completion status, toggle(...) and toggleAll(...), also need to be rewritten to use Dgraph transactions, but this exercise is left to the reader ;).

Hereā€™s the final product:

Summary

We have built a web app that is capable of accessing Dgraph to store and display data. To do that we have learned how to run Dgraph inside Docker containers, adjusted schema in Ratel, and learned how to send GraphQL+- queries to read data and to commit transactions in order to insert or modify data in Dgraph.

There are many more things a developer of a real-life application would need to implement. Most notably Iā€™ve omitted access control and backups, among other things. However, I hope this tutorial gave you an interesting and useful starting point for creating your own Dgraph-powered applications.


This is a companion discussion topic for the original entry at https://blog.dgraph.io/post/building-todo-list-react-dgraph/
2 Likes

nice article, I hope a future post about some integration with nodejs or go graphql server ;D

1 Like

Nice Article - any chance of a similar example using Angular 8? We are currently using Angular 8 with the ApolloAngular client with a node.js graphQL server over a postgresDB. I like what you are doing with Dgraph and would appreciate any examples that would assist migrating from a graphQL/postgresDB backend to Dgraph.

Thank you for the question. I will have our engineering team look at your question and respond promptly.

Porting this example to Angular would mean simply creating an instance of TodoModel and subscribing to its updates / using its methods to write to the database.

Access to fetch at ā€˜http://localhost:8080/commit?startTs=0&abort=trueā€™ from origin ā€˜http://localhost:3000ā€™ has been blocked by CORS policy: No ā€˜Access-Control-Allow-Originā€™ header is present on the requested resource. If an opaque response serves your needs, set the requestā€™s mode to ā€˜no-corsā€™ to fetch the resource with CORS disabled.
Hi there, I have faced this issue when I try to use add new todo item. Please help me! Many thanks.

1 Like

Welcome to Dgraph!

If you are just getting started I would look at using the graphql endpoint. There are some caveats but it is faster IMO to get up and running even faster. Slash makes it even better by making is a fully managed SaaS.

https://graphql.dgraph.io/todo-app-tutorial/

I know what CORS is and how to handle it on an endpoint by fixing headers manually, but donā€™t know how Dgraph handles that, so I canā€™t answer your main question, but maybe this can alleviate your problems.

If you have questions for how the graphql endpoint varies from the Dgraph regular endpoint let me know as that is something I struggled with too getting started.

2 Likes

How do you add types or schema to this project?

Based on reading the documentation I tried to use this command to add a couple of basic types to the Dgraph server that was created with the docker-compose.yml:

$ curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql'

But it didnā€™t update the schema, it only said this:

Dgraph browser is available for running separately using the dgraph-ratel binary

When the database is set up in a single Docker container, that command yields this:

{"data":{"code":"Success","message":"Done"}}

So my question is, how would you modify the schema for Dgraph if the database is set up like it is in the linked tutorial using the docker-compose.yml?

I also had the same problem as BaoNguyen1507

Hi @catherineluse
Please try with the latest docker image. We have come a long way since this tutorial was setup. You can find the latest docker-compose file here.

One thing to note is that you might run into whitelist issue. You should be able to fix that by changing alpha start command to dgraph alpha --my=alpha:7080 --lru_mb=2048 --zero=zero:5080 --whitelist 0.0.0.0/0 for dev environment. I request you to go through this documentation.

If itā€™s the CORS issue can you please try with enabling CORS via plugin/extensions available for major browsers(one such available extension for google chrome can be found here ).

You can use this link to check your browsers CORS policy

It worked after I upgraded dgraph-js-http to version 20.07.0.

using ā€œdgraph-js-httpā€: ā€œ^21.3.0ā€

also updated docker-compose for LOCAL usage / playground. Note this allows all IPs access to alpha (e.g. for updating schema as discussed in tutorial). Wouldnā€™t use this in any ā€œrealā€ config / setup.

May get a ratel login screen, but as long as you have 3 greens, you can just hit continue.

version: "3.2"
services:
  zero:
    image: dgraph/dgraph:v20.11.3
    volumes:
      - type: volume
        source: dgraph
        target: /dgraph
        volume:
          nocopy: true
    ports:
      - 5080:5080
      - 6080:6080
    restart: on-failure
    command: dgraph zero --my=zero:5080
  alpha:
    image: dgraph/dgraph:v20.11.3
    volumes:
      - type: volume
        source: dgraph
        target: /dgraph
        volume:
          nocopy: true
    ports:
      - 8080:8080
      - 9080:9080
    restart: on-failure
    command: dgraph alpha --my=alpha:7080 --lru_mb=2048 --zero=zero:5080 --whitelist=0.0.0.0/0
  ratel:
    image: dgraph/dgraph:v20.11.3
    ports:
      - 8000:8000
    command: dgraph-ratel

volumes:
  dgraph: