DEV Community

Cover image for Build a Decentralized Todo List with React and Blockstack
Dmitri Kyle Brereton
Dmitri Kyle Brereton

Posted on • Originally published at dkb.codes

Build a Decentralized Todo List with React and Blockstack

In this tutorial, you will learn to build a decentralized Todo List using Blockstack and React. Blockstack is a platform that makes it very easy to build decentralized apps. It is faster and more secure to build a simple app using Blockstack authentication and storage than traditional auth/storage methods.

Blockstack's Approach to Decentralization

Big companies like Google and Facebook have centralized databases where they control your data and can do whatever they want with it.

Blockstack Apps allow users to have complete control of their data. No one can access the user's data without their permission. User data is encrypted and stored in private "data lockers", and the user can give an app permission to read/write data to their storage.

In the case of our Todo List app, this means that the app developer will never know what's on your Todo List.

The App

Our Todo List is going to be very simple so we can focus on learning how Blockstack works.

This is what the finished app looks like:

And here is a demo site: https://blockstack-todo-list.netlify.com/

Github Repo: https://github.com/dkb868/secure-todo-list

The Setup

First we will setup the environment. You should have a recent version of node.js installed.

React

We'll be using create-react-app, so type npx create-react-app secure-todo-list into your terminal to create the new project

After a minute or so, it should be complete.

Navigate to your new directory with cd secure-todo-list then type npm start to make sure everything is working well.

You should see this in your browser:

Then open up the project folder in your coding editor and let's do some cleanup. Delete the following files:

  • App.css
  • App.test.js
  • index.css
  • logo.svg

Then open App.js and replace the contents with this:

import React from "react"

class App extends React.Component {
  render() {
    return <div>Nice Meme</div>
  }
}

export default App
Enter fullscreen mode Exit fullscreen mode

And update index.js

import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import * as serviceWorker from "./serviceWorker"

ReactDOM.render(<App />, document.getElementById("root"))

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
Enter fullscreen mode Exit fullscreen mode

Prettier

If you don't use prettier, I highly recommend it. It makes your code much cleaner without any effort. You can add it to your editor by looking for the prettier plugin.

Add a .prettierrc file to your project root directory (secure-todo-list/) with an empty object as the content, which gives you the default settings.

{}
Enter fullscreen mode Exit fullscreen mode

Semantic UI

We'll use Semantic UI, a CSS library, to give our app some styling.

Copy this url (https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css) into your public/index.html by adding this line into the head of your html file.

<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
/>
Enter fullscreen mode Exit fullscreen mode

Now you should have a very beautiful, minimalist site completed.

Blockstack Account

You're going to need a Blockstack Account so you can login and use your app. You can get one by going to https://blockstack.org/ and selecting Create ID from the menu.

A Simple Todo List

We're going to start by building a simple todo list in React without any Blockstack. The app state will be lost whenever the page is refreshed, but this will make it easier to see where blockstack comes in.

Initial State

Let's start by adding some state to our App. Add this above the render function in App.js

state = {
  todos: [
    {
      id: 1,
      title: "Wash the dishes",
      done: false,
    },
    {
      id: 2,
      title: "Clean my room",
      done: false,
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

Now our app keeps track of todos, which have three atributes:

  • id: A unique identifier for this todo item
  • title: The name given to this task
  • done: Whether or not this task has been completed

Displaying the Todos

Now that we have some todos, let's display them on the page.

Change your render method to the following:

  render() {
    return (
      <div style={{ padding: "30px 0" }}
        className="ui text container center aligned">
        <h2>My Todos</h2>
        <div className="ui grid">
          <div className="row centered">
            <div className="column twelve wide">
              <div className="grouped fields">
                {this.state.todos
                  .filter(todo => !todo.done)
                  .map(todo => (
                    <div key={todo.id} className="field">
                      <div className="ui checkbox">
                        <input type="checkbox" />
                        <label>{todo.title}</label>
                      </div>
                    </div>
                  ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

All the classnames like ui text container center aligned are from Semantic UI and help make our app look nicer.

The line this.state.todos.filter(todo => !todo.done).map(todo => ... filters out todos that are already done and hides them from the page.

Now you should have something that looks like a todo list.

If you click on one of those checkboxes, you'll realize that it does nothing. Ideally we want things to disappear when we check them, so let's add that in.

Completing Todos

Add an onClick handler to the checkbox.

<input
  type="checkbox"
  onClick={() => {
    this.handleCheckboxClick(todo.id)
  }}
/>
Enter fullscreen mode Exit fullscreen mode

We use a slightly strange syntax because we want to pass in the id of the selected todo to our handler function.

The handler should be added above the render function.

  handleCheckboxClick(id) {
    let newTodos = [...this.state.todos];
    newTodos[newTodos.findIndex(todo => todo.id === id)].done = true;
    this.setState({
      todos: newTodos
    });
  }
Enter fullscreen mode Exit fullscreen mode

This is one of the many ways to modify array state in React. First we make a copy of the current list of todos, then we mark the selected todo (identified by its id) as done and update the state.

Now when you check the box, the todo should vanish from the page, since we filter out any item that is marked as done.

Adding Todos

In real life, people probably have more tasks to do than washing dishes and cleaning their room, so let's allow users to add their own todos.

First let's add an input form to our render method.

render() {
    return (
      <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <h2>My Todos</h2>
        <div className="ui grid">
          <div className="row centered">
            <div className="column twelve wide">
              <form className="ui form" onSubmit={this.handleAddTodoClick}>
                <div className="inline fields">
                  <div className="twelve wide field">
                    <input
                      type="text"
                      value={this.state.newTodo}
                      onChange={this.hanldeInputChange}
                    />
                  </div>
                  <button className="ui button primary" type="submit">
                    Add todo
                  </button>
                </div>
              </form>
            </div>
          </div>
          <div className="row centered">
            <div className="column twelve wide">
              <div className="grouped fields">
                {this.state.todos
                  .filter(todo => !todo.done)
                  .map(todo => (
                    <div key={todo.id} className="field">
                      <div className="ui checkbox">
                        <input
                          type="checkbox"
                          onClick={() => {
                            this.handleCheckboxClick(todo.id);
                          }}
                        />
                        <label>{todo.title}</label>
                      </div>
                    </div>
                  ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Then let's implement all those handler functions.

Update the initial state to keep track of our new todo value, and cleanup those default todos

state = {
  todos: [],
  newTodo: "",
}
Enter fullscreen mode Exit fullscreen mode

Implement the handleInputChange function which will keep track of what the user is entering.

hanldeInputChange = e => {
  this.setState({
    newTodo: e.target.value,
  })
}
Enter fullscreen mode Exit fullscreen mode

Next we implement handleAddTodoClick which is called when the user hits enter or clicks the button to add their new todo item.

handleAddTodoClick = e => {
  e.preventDefault()
  const newTodo = {
    id: this.state.todos.length + 1,
    title: this.state.newTodo,
    done: false,
  }
  const todos = [...this.state.todos]
  todos.push(newTodo)
  this.setState({
    todos: todos,
    newTodo: "",
  })
}
Enter fullscreen mode Exit fullscreen mode

Your entire App.js should look like this:

import React from "react"

class App extends React.Component {
  state = {
    todos: [],
    newTodo: "",
  }

  handleCheckboxClick(id) {
    let newTodos = [...this.state.todos]
    newTodos[newTodos.findIndex(todo => todo.id === id)].done = true
    this.setState({
      todos: newTodos,
    })
  }

  handleAddTodoClick = e => {
    e.preventDefault()
    const newTodo = {
      id: this.state.todos.length + 1,
      title: this.state.newTodo,
      done: false,
    }
    const todos = [...this.state.todos]
    todos.push(newTodo)
    this.setState({
      todos: todos,
      newTodo: "",
    })
  }

  hanldeInputChange = e => {
    this.setState({
      newTodo: e.target.value,
    })
  }

  render() {
    return (
      <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <h2>My Todos</h2>
        <div className="ui grid">
          <div className="row centered">
            <div className="column twelve wide">
              <form className="ui form" onSubmit={this.handleAddTodoClick}>
                <div className="inline fields">
                  <div className="twelve wide field">
                    <input
                      type="text"
                      value={this.state.newTodo}
                      onChange={this.hanldeInputChange}
                    />
                  </div>
                  <button className="ui button primary" type="submit">
                    Add todo
                  </button>
                </div>
              </form>
            </div>
          </div>
          <div className="row centered">
            <div className="column twelve wide">
              <div className="grouped fields">
                {this.state.todos
                  .filter(todo => !todo.done)
                  .map(todo => (
                    <div key={todo.id} className="field">
                      <div className="ui checkbox">
                        <input
                          type="checkbox"
                          onClick={() => {
                            this.handleCheckboxClick(todo.id)
                          }}
                        />
                        <label>{todo.title}</label>
                      </div>
                    </div>
                  ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

export default App
Enter fullscreen mode Exit fullscreen mode

Now you should be able to add new todo items, and check them off. The only problem is that when you refresh the page, you lose all of your precious todos. Now it's time to actually save our todos using Blockstack.

Let's Add Blockstack!

Now we'll add user authentication and storage using Blockstack. First stop your app with ctrl-c and install blockstack with npm install blockstack. Then we can start the app again with npm start and everything should still be working the same.

Authentication

Setup blockstack in App.js by adding the following lines above the class declaration.

import { UserSession, AppConfig } from "blockstack";

const appConfig = new AppConfig(["store_write"]);
const userSession = new UserSession({ appConfig: appConfig });

class App extends React.Component {
    ...
}
Enter fullscreen mode Exit fullscreen mode

The line const appConfig = new AppConfig(["store_write"]); is used to set the configuration of our blockstack app. You can request the permissions you want from the user. In this case, we request store_write permissions, which allows us to store data in the user's private storage.

If we wanted to build something more social, we would want publish_data permissions, which allows certain user data to be visible to other users.

const userSession = new UserSession({ appConfig: appConfig }); establishes a user session, which allows us to handle authentication.

Add a login button to the top of the page.

<div style={{ padding: "30px 0" }} className="ui text container center aligned">
  <button className="ui button positive" onClick={this.handleSignIn}>
    Sign in with blockstack
  </button>
  <h2>My Todos</h2>
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

And implement our handler function this.handleSignIn like this:

handleSignIn = () => {
  userSession.redirectToSignIn()
}
Enter fullscreen mode Exit fullscreen mode

Yep, it takes one line of code to implement sign in.

Your page should now look like this:


Let's click on that button and see what happens!

Well, we're taken to the blockstack browser for login, but it looks like there's a problem...

Hmm, "Failed to fetch information about the app requesting authentication. Please contact the app maintainer to resolve the issue." That's not very descriptive, but our console says something a little more useful.

Access to fetch at 'http://localhost:3000/manifest.json' from origin 'https://browser.blockstack.org' 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.
Enter fullscreen mode Exit fullscreen mode

What is going on here? This is actually a very common bug when you're just getting started with Blockstack, so let's talk about it.

Fixing The CORS Error

The issue is that the Blockstack Browser is trying to access a file from your website called manifest.json, which contains information about your app. However, due to CORS, websites can't make requests to other websites on a different domain by default. This is done for security purposes. So our website right now is rejecting the Blockstack Browser's request for our manifest.json but we actually want Blockstack to be able to access that file.

To do that, we'll need to modify our webpack config. Since we used create-react-app , the webpack config is hidden. To modify it, we use the command npm run eject . You will probably get a warning about having untracked files and uncommitted changes. So commit all your changes to git first.

git add -A
git commit -m "did things"
npm run eject
Enter fullscreen mode Exit fullscreen mode

You'll see two new folders in your directory called scripts and config. Go to config/webpackDevServer.config.js and add the following line on top of the module exports function.

module.exports = function(proxy, allowedHost) {
  return {
    headers: {
      "Access-Control-Allow-Origin": "*"
    },

    // WebpackDevServer 2.4.3 introduced a security fix that prevents remote
    // websites from potentially accessing local content through DNS rebinding:

    ...

  }
}
Enter fullscreen mode Exit fullscreen mode

Now start the project again with npm start and let's try logging in again.

Our app could probably use a better name than "Create React App Sample" so let's go to public/manifest.json to modify that. You can change the app name to anything you like here.

{
  "short_name": "Todo List",
  "name": "Secure Todo List",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}
Enter fullscreen mode Exit fullscreen mode

Authentication Continued

Now let's modify the view based on whether or not the user is logged in. Logged out users probably shouldn't see their todo list, and logged in users don't need to see the login button.

To make this a bit cleaner, we're going to separate those two things into different components. We'll have a TodoList component which shows the Todo List and a Login component which shows the login page.

Copy the contents of App.js into a new file called TodoList.js and modify it as follows.

import React from "react"

class TodoList extends React.Component {
  state = {
    todos: [],
    newTodo: "",
  }

  handleCheckboxClick(id) {
    let newTodos = [...this.state.todos]
    newTodos[newTodos.findIndex(todo => todo.id === id)].done = true
    this.setState({
      todos: newTodos,
    })
  }

  handleAddTodoClick = e => {
    e.preventDefault()
    const newTodo = {
      id: this.state.todos.length + 1,
      title: this.state.newTodo,
      done: false,
    }
    const todos = [...this.state.todos]
    todos.push(newTodo)
    this.setState({
      todos: todos,
      newTodo: "",
    })
  }

  hanldeInputChange = e => {
    this.setState({
      newTodo: e.target.value,
    })
  }

  render() {
    return (
      <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <h2>My Todos</h2>
        <div className="ui grid">
          <div className="row centered">
            <div className="column twelve wide">
              <form className="ui form" onSubmit={this.handleAddTodoClick}>
                <div className="inline fields">
                  <div className="twelve wide field">
                    <input
                      type="text"
                      value={this.state.newTodo}
                      onChange={this.hanldeInputChange}
                    />
                  </div>
                  <button className="ui button primary" type="submit">
                    Add todo
                  </button>
                </div>
              </form>
            </div>
          </div>
          <div className="row centered">
            <div className="column twelve wide">
              <div className="grouped fields">
                {this.state.todos
                  .filter(todo => !todo.done)
                  .map(todo => (
                    <div key={todo.id} className="field">
                      <div className="ui checkbox">
                        <input
                          type="checkbox"
                          onClick={() => {
                            this.handleCheckboxClick(todo.id)
                          }}
                        />
                        <label>{todo.title}</label>
                      </div>
                    </div>
                  ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
}

export default TodoList
Enter fullscreen mode Exit fullscreen mode

Then make a Login.js component like this.

import React from "react"

class Login extends React.Component {
  handleSignIn = () => {
    this.props.userSession.redirectToSignIn()
  }

  render() {
    return (
      <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <h1>Decentralized Todo List</h1>
        <p>This is the most secure todo list on the market.</p>

        <button className="ui button positive" onClick={this.handleSignIn}>
          Sign in with blockstack
        </button>
      </div>
    )
  }
}

export default Login
Enter fullscreen mode Exit fullscreen mode

We pass in the userSession as props. This object contains helpful functions to do with user authentication.

Finally our App.js will be modified to show the Login component when the user is logged out, and the TodoList when the user is logged in.

import React from "react"
import { UserSession, AppConfig } from "blockstack"
import Login from "./Login"
import TodoList from "./TodoList"
const appConfig = new AppConfig(["store_write"])
const userSession = new UserSession({ appConfig: appConfig })

class App extends React.Component {
  render() {
    return (
      <div>
        {userSession.isUserSignedIn() ? (
          <TodoList userSession={userSession} />
        ) : (
          <Login userSession={userSession} />
        )}
      </div>
    )
  }
}

export default App
Enter fullscreen mode Exit fullscreen mode

We use the function userSession.isUserSignedIn() to find out whether there is a logged in user or not.

Now you should see the login page by default. When you click the button, you are redirected to Blockstack, then once you select your id you are redirected to your app, then...it still shows you the login page. What's up with that?

Turns out we're actually in an intermediary login stage. By this point, Blockstack has given the app a token with all of the user information. We need to add one more function call to extract information from that toke and finish the sign in.

Add these lines above the render() function in your App component.

 componentWillMount() {
    if (userSession.isSignInPending()) {
      userSession
        .handlePendingSignIn()
        .then(() => {
          window.location = window.location.origin;
        })
        .catch(err => console.log(err));
    }
  }
Enter fullscreen mode Exit fullscreen mode

This extracts the user information from the token, and completes the sign in, then refreshes the page.

Here is a chart that explains the whole Blockstack authentication process.

With this in place, try logging in again and you should be redirected to the todo list.

Lastly, let's add a sign out button to the todo list page. Go to TodoList.js and add a button to the top of the page in the render function.

 <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <button className="ui button negative" onClick={this.handleSignout}>
          Sign out
        </button>

        <h2>My Todos</h2>
        <div className="ui grid">

            ...

     </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Add the handleSignout function somewhere above the render function.

handleSignout = () => {
  this.props.userSession.signUserOut(window.location.origin)
}
Enter fullscreen mode Exit fullscreen mode

Now you can login and logout of the app with Blockstack.

Storing The Todos

Now that the user can login to our app, we can store their data with Blockstack.

We'll be using two core functions of the blockstack.js library: putFile and getFile.

They do exactly what they sound like. putFile allows you to store files, and getFile allows you to retrieve files. You can store any type of file, and they can be encrypted if you want.

In our case, we'll be storing our todos in JSON format because it makes them easy to handle.

Go to TodoList.js and modify the handleAddTodoClick function as follows:

handleAddTodoClick = e => {
  e.preventDefault()
  const newTodo = {
    id: this.state.todos.length + 1,
    title: this.state.newTodo,
    done: false,
  }
  const todos = [...this.state.todos]
  todos.push(newTodo)
  const options = { encrypt: true }
  this.props.userSession
    .putFile("todos.json", JSON.stringify(todos), options)
    .then(() => {
      this.setState({
        todos,
        newTodo: "",
      })
    })
}
Enter fullscreen mode Exit fullscreen mode

This stores all the user's todos in a file called todos.json

Modify handleCheckboxClick so that when we mark todos as done, this is also updated in the user storage.

  handleCheckboxClick(id) {
    let newTodos = [...this.state.todos];
    newTodos[newTodos.findIndex(todo => todo.id === id)].done = true;
    const options = { encrypt: true };
    this.props.userSession
      .putFile("todos.json", JSON.stringify(newTodos), options)
      .then(() => {
        this.setState({
          todos: newTodos
        });
      });
  }
Enter fullscreen mode Exit fullscreen mode

Try making some todos now and you should see something like this in your console, indicating that the files were stored.

If you refresh the page you won't see anything, because we still need to retrieve the todos.

Add a new function to your class called fetchData which will get the todo list from user storage.

  async fetchData() {
    const options = { decrypt: true };
    const file = await this.props.userSession.getFile("todos.json", options);
    let todos = JSON.parse(file || "[]");
    this.setState({
      todos
    });
  }

Enter fullscreen mode Exit fullscreen mode

We will call this function in our componentDidMount

componentDidMount() {
    this.fetchData();
}
Enter fullscreen mode Exit fullscreen mode

Now you can add a todo item, refresh your page, and it will still be there!

Adding User Profile Data

Right now our app doesn't feel very personal, but we can use Blockstack to get information like the user's name to customize their experience.

Add a new field to the state to store the user object.

state = {
  newTodo: "",
  todos: [],
  user: null,
}
Enter fullscreen mode Exit fullscreen mode

Then modify the fetchData function to update the state with user info.

  async fetchData() {
    const options = { decrypt: true };
    const file = await this.props.userSession.getFile("todos.json", options);
    let todos = JSON.parse(file || "[]");
    this.setState({
      todos,
      user: new Person(this.props.userSession.loadUserData().profile)
    });
  }
Enter fullscreen mode Exit fullscreen mode

And add an import statement at the top of your file.

import { Person } from "blockstack"
Enter fullscreen mode Exit fullscreen mode

The Person object puts the user data in an easily accessible format.

Modify the render function to display some user information. We'll be showing their name and profile image.

render() {
    const { user } = this.state;

    return (
      <div
        style={{ padding: "30px 0" }}
        className="ui text container center aligned"
      >
        <button className="ui button negative" onClick={this.handleSignout}>
          Sign out
        </button>
        <h1>{user && user.name()}</h1>
        <img
          className="ui centered medium rounded image"
          src={user && user.avatarUrl()}
          alt="user profile image"
        />
        <h2>My Todos</h2>

        ...
Enter fullscreen mode Exit fullscreen mode

Now the app should feature the user's name and profile image.


Our app looks good to go, now let's deploy it for the rest of the world to see.

Deploying To Netlify

There are many ways to deploy your React app, but Netlify is one of the best. It allows you to easily setup continuous deployment.

First let's make a new repository on github.

Add and commit all of your files.

git add -A
git commit -m "made everything"
Enter fullscreen mode Exit fullscreen mode

Then follow the commands to push an existing repository. For me that would be:

git remote add origin https://github.com/dkb868/secure-todo-list.git
git push -u origin master
Enter fullscreen mode Exit fullscreen mode

Now you should have a beautiful new repo up on github.

Make an account on Netlify, then in your dashboard, select "New site from Git".

Select Github, and search for your repo.

Use the following build settings, then click Deploy Site

Give it a few minutes, then you should have your site up at something.netlify.com. You can modify this name if you want, or add a custom domain.

If we go to our newly launched app, we'll see a familiar error.

We know this is a CORS error, and we fixed it in our development environment, so now we need to fix it in production.

With Netlify, this is as simple as adding a netlify.toml file in your root project directory.

[[headers]]
  for = "/*"
  [headers.values]
  Access-Control-Allow-Origin = "*"
Enter fullscreen mode Exit fullscreen mode

Add that file and push it to GitHub. Once you have continuous deploy enabled, it will be deployed automatically in a few minutes.

Now everything should be working great.

Conclusion

If you made it this far, congrats for finishing the app!

If you got lost at some point, you can check out the github repo or the demo website for reference.

Demo Website: https://blockstack-todo-list.netlify.com/

Github Repo: https://github.com/dkb868/secure-todo-list

This is my first coding tutorial, so if you have any feedback on things I can improve, please let me know.

Also, Blockstack has a hackathon going on right now that you can participate in!

Top comments (1)

Collapse
 
polluterofminds profile image
Justin Hunter

Great tutorial! I seriously wish there had been detailed tutorials like this when I started building on Blockstack. Nice work.