Build a Decentralized Todo List with React and Blockstack
Dmitri Kyle Brereton
Posted on August 1, 2019
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
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()
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.
{}
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"
/>
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,
},
],
}
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>
);
}
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)
}}
/>
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
});
}
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>
);
}
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: "",
}
Implement the handleInputChange
function which will keep track of what the user is entering.
hanldeInputChange = e => {
this.setState({
newTodo: e.target.value,
})
}
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: "",
})
}
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
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 {
...
}
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>
And implement our handler function this.handleSignIn
like this:
handleSignIn = () => {
userSession.redirectToSignIn()
}
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.
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
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:
...
}
}
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"
}
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
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
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
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));
}
}
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>
Add the handleSignout
function somewhere above the render
function.
handleSignout = () => {
this.props.userSession.signUserOut(window.location.origin)
}
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: "",
})
})
}
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
});
});
}
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
});
}
We will call this function in our componentDidMount
componentDidMount() {
this.fetchData();
}
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,
}
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)
});
}
And add an import statement at the top of your file.
import { Person } from "blockstack"
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>
...
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"
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
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 = "*"
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!
Posted on August 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.