{"id":26121924,"url":"https://github.com/ipfs-shipyard/workshop-todo-dapp","last_synced_at":"2025-04-13T13:06:15.316Z","repository":{"id":43644497,"uuid":"141710967","full_name":"ipfs-shipyard/workshop-todo-dapp","owner":"ipfs-shipyard","description":"A workshop into adding realtime collaboration in a typical To-do app","archived":false,"fork":false,"pushed_at":"2023-01-04T08:19:13.000Z","size":3540,"stargazers_count":30,"open_issues_count":22,"forks_count":2,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-27T04:09:46.901Z","etag":null,"topics":["dapp","example","p2p","peer-star-app","todomvc"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ipfs-shipyard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-20T12:44:51.000Z","updated_at":"2024-09-25T07:41:50.000Z","dependencies_parsed_at":"2023-02-02T05:00:25.304Z","dependency_job_id":null,"html_url":"https://github.com/ipfs-shipyard/workshop-todo-dapp","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipfs-shipyard%2Fworkshop-todo-dapp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipfs-shipyard%2Fworkshop-todo-dapp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipfs-shipyard%2Fworkshop-todo-dapp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ipfs-shipyard%2Fworkshop-todo-dapp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ipfs-shipyard","download_url":"https://codeload.github.com/ipfs-shipyard/workshop-todo-dapp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248717242,"owners_count":21150389,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dapp","example","p2p","peer-star-app","todomvc"],"created_at":"2025-03-10T14:37:28.735Z","updated_at":"2025-04-13T13:06:15.298Z","avatar_url":"https://github.com/ipfs-shipyard.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# workshop-todo-dapp\n\nThis walk-through will guide you into the process of converting a local To-do application into a decentralized application that allows different users to manipulate the To-dos collaboratively and in realtime, while also offering offline support.\n\nThe project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) and is highly based on the [TodoMVC](http://todomvc.com/) project.\n\n## Walk-through\n\nFollow the step-by-step walk-through below to complete the workshop.\n\nAt any time, you may check the final application in the [`with-peer-base`](https://github.com/ipfs-shipyard/workshop-todo-dapp/compare/master...with-peer-base) branch in case you are running into issues.\n\n1. [Installing](#1-installing)\n1. [Running](#2-running)\n1. [Understanding the To-dos store and data-model](#3-understanding-the-to-dos-store-and-data-model)\n1. [Adapting the To-dos store to use `peer-base`](#4-adapting-the-to-dos-store-to-use-peer-base)\n    1. [Install `peer-base`](#41-install-peer-base)\n    1. [Create the app](#42-create-the-app)\n    1. [Re-implement the `load` method](#43-re-implement-the-load-method)\n    1. [Get rid of the `localStorage`](#44-get-rid-of-the-localstorage)\n    1. [Update `add`, `remove`, `updateTitle` and `updateCompleted`](#45-update-add-remove-updatetitle-and-updatecompleted)\n1. [Testing if the application works locally](#5-testing-if-the-application-works-locally)\n1. [Displaying the number of users](#6-displaying-the-number-of-users-peers)\n    1. [Replicate the `subscribe` and `publishStateChange` but for the peers](#61-replicate-the-subscribe-and-publishstatechange-but-for-the-peers)\n    1. [Keep track of `peersCount` in the UI](#62-keep-track-of-peerscount-in-the-ui)\n    1. [Render `peersCount` in the UI](#63-render-peerscount-in-the-ui)\n    1. [Style `peersCount` in the UI](#64-style-peerscount-in-the-ui)\n1. [Testing if the application works with other users](#7-testing-if-the-application-works-with-other-users)\n1. [Deploying the application on IPFS](#8-deploying-the-application-on-ipfs)\n    1. [Install IPFS and run a local node](#81-install-ipfs-and-run-a-local-node)\n    1. [Ensure links are relative](#82-ensure-links-are-relative)\n    1. [Build and deploy](#83-build-and-deploy)\n    1. [Using a domain](#84-using-a-domain)\n\n### 1. Installing\n\nBe sure to have [Node.js](https://nodejs.org/download/) in your machine. Install the project by running:\n\n```sh\n$ npm install\n```\n\n### 2. Running\n\nNow that the project is installed, you may run the development server:\n\n```sh\n$ npm start\n```\n\nThe application will open automatically in your browser once ready.\n\n### 3. Understanding the To-dos store and data-model\n\nThe [`App`](src/App.js) component is our root react component. When mounted, it loads the initial To-dos from the `todos-store` and subscribes to subsequent updates. This ensures that any change to the To-dos state will trigger a UI update. While you can explore the `App` component and all its underlying logic, our goal is to change it as little as possible.\n\nThe [`todos-store`](src/todos-store.js) exposes all the operations necessary to read and manipulate the To-dos. It also continuously persists the state to the `localStorage` so that the To-dos can be restored on subsequent visits. Moreover, it allows subscribers to receive the new state whenever it's updated. The state looks like this:\n\n```js\n[\n    { id: \"\u003cunique-id\u003e\", title: \"Buy candies\": completed: true },\n    { id: \"\u003cunique-id\u003e\", title: \"Walk the dog\", completed: false }\n]\n```\n\nAs you imagine, this application only works locally within the browser. It doesn't allow different users to read and manipulate the To-dos. What architectural pieces would we traditionally need to support [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations amongs several users?\n\n- A database to store the To-dos\n- A (Restful) API that offers a CRUD for the To-dos\n- A static web-server to serve the application assets\n- A realtime server, using [`socket.io`](https://socket.io/) or similar, to deliver updates to the users without using polling mechanisms\n\nBut even so, how do we deal with concurrent updates? What if we want to allow users to performs changes while offline and sync them when online? These are [hard problems](https://www.youtube.com/watch?v=4VB66hJSvqM) to solve unless we use the right technologies.\n\nThis is where `peer-base` comes in. It's goal is to provide the primitives for developers to build real-time and offline-first decentralized applications by using ([delta](https://github.com/ipfs-shipyard/js-delta-crdts)) [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) and [IPFS](https://ipfs.io/).\n\n### 4. Adapting the To-dos store to use `peer-base`\n\n#### 4.1. Install `peer-base`\n\nInstall `peer-base` by running:\n\n```sh\n$ npm install peer-base\n```\n\n#### 4.2. Create the app\n\nCreate a new app with the name `todo-dapp`:\n\n```js\n// src/todos-store.js\nimport createApp from 'peer-base';\n\n// ...\n\nconst app = createApp('todo-dapp');\n\napp.on('error', (err) =\u003e console.error('error in app:', err));\n```\n\nWe will just log `error` events in the console, but you could display them in the UI instead.\n\n#### 4.3. Re-implement the `load` method\n\nWe need to re-implement the `load` function which is responsible for loading the todos. In here, we must:\n\n- Start the app\n- Create a new collaboration for the To-dos\n- Subscribe to the `state changed` event of the collaboration to receive updates to the underlying To-dos\n- Update `todos` variable to the last known list of To-dos\n\nIn `peer-base` we may have as many collaborations as we want. Users collaborate on a CRDT [type](https://github.com/ipfs-shipyard/js-delta-crdts#types): either a built-in or a custom one. Because the To-dos data-model is an array of objects, we will use the `rga` (Replicable Growable Array) type.\n\nThis allows multiple users to perform concurrent CRUD operations without any conflicts. This works well in most cases but it doesn't allow concurrent updates of the title and complete fields of To-dos. That could be supported by using sub-collaborations  but we will skip it for the sake of simplicity.\n\nUpdate the `load` function like so:\n\n```js\n// src/todos-store.js\n\n// ...\nlet collaboration;\n\nexport default {\n    async load() {\n        await app.start();\n\n        collaboration = await app.collaborate('todos-of-\u003cgithub-username\u003e', 'rga');\n\n        collaboration.removeAllListeners('state changed');\n        collaboration.on('state changed', () =\u003e {\n            todos = collaboration.shared.value();\n            publishStateChange(todos);\n        });\n\n        todos = collaboration.shared.value();\n\n        return todos;\n    },\n\n    // ...\n};\n```\n\nBe sure to change `\u003cgithub-username\u003e` so that the Collaboration ID is unique. This ensures that you start with an empty list of To-dos.\n\n\nThe `collaboration.shared` is a reference to the CRDT instance. We will use it in next steps to perform changes on the underlying state.\n\nNote that we are calling `removeAllListeners('state changed')` so that `load` can be called multiple times during the lifecyle of the app. If we didn't do that, the subscribers would be called multiple times for the same event.\n\n\n#### 4.4. Get rid of the `localStorage`\n\nPeer-star already persists the last known state of each collaboration. This means that we can safely remove any code that relates to storing or reading the To-dos from the `localStorage`.\n\nYou may remove all the lines below:\n\n```js\n// src/todos-store.js\n// ....\n\nimport throttle from 'lodash/throttle';\n\n// ....\n\nwindow.addEventListener('unload', () =\u003e saveTodos(todos));\n\nconst readTodos = () =\u003e JSON.parse(localStorage.getItem('dapp-todos') || '[]');\nconst saveTodos = () =\u003e todos \u0026\u0026 localStorage.setItem('dapp-todos', JSON.stringify(todos));\nconst saveTodosThrottled = throttle(saveTodos, 1000, { leading: false });\n```\n\n... and the `publishStateChange` now becomes simpler:\n\n```js\n// src/todos-store.js\n\nconst publishStateChange = (todos) =\u003e subscribers.forEach((listener) =\u003e listener(todos));\n```\n\nYeah, less code yields profit!\n\n### 4.5. Update `add`, `remove`, `updateTitle` and `updateCompleted`\n\nWe now must update `add`, `remove`, `updateTitle` and `updateCompleted` functions to call the CRDT mutations instead of manipulating the `todos` manually. By doing so, we will trigger a `state change` event on ourselves and in other replicas (users) as well.\n\nThe `rga` CRDT type has the following methods:\n\n- `push()`\n- `insertAt(index, value)`\n- `updateAt(index, value)`\n- `removeAt(index)`\n\nLet's use these functions to mutate the state:\n\n```js\n// src/todos-store.js\n// ...\n\nexport default {\n    // ...\n    add(title) {\n        collaboration.shared.push({ id: uuidv4(), title, completed: false });\n    },\n\n    remove(id) {\n        const index = todos.findIndex((todo) =\u003e todo.id === id);\n\n        if (index === -1) {\n            return;\n        }\n\n        collaboration.shared.removeAt(index);\n    },\n\n    updateTitle(id, title) {\n        const index = todos.findIndex((todo) =\u003e todo.id === id);\n        const todo = todos[index];\n\n        if (!todo || todo.title === title) {\n            return;\n        }\n\n        const updatedTodo = { ...todo, title };\n\n        collaboration.shared.updateAt(index, updatedTodo);\n    },\n\n    updateCompleted(id, completed) {\n        const index = todos.findIndex((todo) =\u003e todo.id === id);\n        const todo = todos[index];\n\n        if (!todo || todo.completed === completed) {\n            return;\n        }\n\n        const updatedTodo = { ...todo, completed };\n\n        collaboration.shared.updateAt(index, updatedTodo);\n    },\n\n    // ...\n},\n```\n\nAnd that's all. Easy huh?\n\n### 5. Testing if the application works locally\n\nYou may test the changes we made locally. The application should behave exactly the same as before but it's now partially decentralized! It's not totally decentralized because we are still serving it using a web server. But more on that later.\n\n### 6. Displaying the number of users (peers)\n\nThe `collaboration` emits the `membership changed` event that we can listen to keep track of the peers collaborating. We will use it to display the number of peers in the UI.\n\n### 6.1. Replicate the `subscribe` and `publishStateChange` but for the peers\n\nLets replicate the `subscribe` and `publishStateChange` logic but for the peers:\n\n```js\n// src/todos-store.js\n// ...\nconst peersSubscribers = new Set();\n\nconst publishPeersChange = (peers) =\u003e peersSubscribers.forEach((listener) =\u003e listener(peers));\n\nexport default {\n    async load() {\n        // ...\n\n        collaboration.removeAllListeners('membership changed');\n        collaboration.on('membership changed', publishPeersChange);\n    },\n\n    // ...\n\n    subscribePeers(subscriber) {\n        peersSubscribers.add(subscriber);\n\n        return () =\u003e peersSubscribers.remove(subscriber);\n    },\n};\n```\n\n### 6.2. Keep track of `peersCount` in the UI\n\nAdd `peersCount` to the `App` component state and update it whenever it changes:\n\n```js\n// src/App.js\n// ...\n\nclass App extends Component {\n    state = {\n        // ...\n        peersCount: 1,\n    }\n\n    async componentDidMount() {\n        // ...\n\n        todosStore.subscribePeers((peers) =\u003e this.setState({ peersCount: peers.size }));\n    }\n\n    // ...\n}\n```\n\n### 6.3. Render `peersCount` in the UI\n\nLet's render `peersCount` in the footer:\n\n```jsx\n// src/App.js\n// ...\n\nclass App extends Component {\n    // ...\n\n    render() {\n        const { loading, error, todos, peersCount } = this.state;\n\n        return (\n            \u003cdiv className=\"App\"\u003e\n                { /* ... */ }\n\n                \u003cfooter className=\"App__footer\"\u003e\n                    \u003cdiv className=\"App__peers-count\"\u003e{ peersCount }\u003c/div\u003e\n\n                    { /* ... */ }\n                \u003c/footer\u003e\n            \u003c/div\u003e\n        );\n    }\n);\n```\n\n### 6.4. Style `peersCount` in the UI\n\nFinally, add the `App__peers-count` CSS class to the bottom of `App.css`:\n\n```css\n/* App.css */\n/* ... */\n\n.App__peers-count {\n    width: 50px;\n    height: 50px;\n    margin-bottom: 25px;\n    padding: 5px;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    border: 1px solid #cc9a9a;\n    background-color: rgba(175, 47, 47, 0.15);\n    border-radius: 50%;\n    color: #6f6f6f;\n    font-size: 15px;\n    line-height: 50px;\n}\n```\n\nYou should now be able to see the number of peers collaborating on the To-dos. Depending on the network, it might take some time to discover peers.\n\n### 7. Testing if the application works with other users\n\nOpen the application in two different browsers, e.g.: Chrome and Chrome incognito. Any changes should replicate seamlessly. Be sure to also make changes while offline and see if they syncronize correctly once online.\n\n### 8. Deploying the application on IPFS\n\nInstead of using a regular web server to serve the application, we will use IPFS instead. After completing this step, our app will be 100% decentralized!\n\n#### 8.1. Install IPFS and run a local node\n\nWe need to have a local IPFS node for the upcoming steps. We will use a [JS IPFS node](https://github.com/ipfs/js-ipfs) but you could use a [Go node](https://github.com/ipfs/go-ipfs) instead.\n\nLet's install it globally:\n\n```sh\n$ npm install -g ipfs\n```\n\nNow, open a new terminal window and start the node:\n\n```sh\n$ jsipfs init\n$ jsipfs daemon\n```\n\n#### 8.2. Ensure links are relative\n\nEverything stored on IPFS is immutable and content-addressable. When a file is stored, IPFS calculates its hash and uses it as an identifier, called `cid`. These files can be accessed in your browser via IPFS node gateways. Since we are running the JS IPFS node, we can access files via `http://localhost:9090/ipfs/\u003ccid\u003e`.\n\nBecause the `cid` is unknown at build time, we can't use absolute paths to reference any links or assets. Luckily for us, Create React App allows us to set a `homepage` property which will be used to prefix every asset:\n\n```json\n{\n    \"name\": \"workshop-todo-dapp\",\n    \"homepage\": \".\",\n    \"...other\": \"properties\"\n}\n```\n\n#### 8.3. Build and deploy\n\nLet's create a production-ready version of the website by building it:\n\n```sh\n$ npm run build\n```\n\nThis creates a `build` folder with all the application assets. Let's deploy it to IPFS by adding that folder to your local IPFS node:\n\n```sh\n$ jsipfs add -r build\n```\n\nThe `-r` tell `ipfs` to recursively add all the files. At the end of the command output, you should see the `cid` of all added files, including the build folder one:\n\n```\n...\nadded QmcFc6EPhavNSfdjG8byaxxV6KtHZvnDwYXLHvyJQPp3uN public/favicon.ico\nadded \u003ccid\u003e build\n```\n\nFinally, copy the `\u003ccid\u003e` and use your local IPFS node to access the website: `http://localhost:9090/ipfs/\u003ccid\u003e`. The trick here is that other IPFS nodes that pin the same `\u003ccid\u003e` will also be eligible to serve the website!\n\n#### 8.4. Using a domain\n\nIn order to use a domain with your website deployed on IPFS, we need to first understand dnslink. Please watch [\"Quick explanation of dnslink in IPFS\"](https://www.youtube.com/watch?v=YxKZFeDvcBs) by [@VictorBjelkholm](https://github.com/VictorBjelkholm) that explains what dnslink is in less than 3 minutes.\n\nFirst, create to a ALIAS record pointing to a public Gateway (e.g.: gateway-int.ipfs.io) or a A record pointing to the IP address where your IPFS gateway is running. Lastly, create a TXT record named `_dnslink.\u003cdomain\u003e` with a value of `dnslink=/ipfs/\u003ccid\u003e` (replace `\u003cdomain\u003e` and `\u003ccid\u003e` with the correct values). We recommend setting a short TTL like 60 seconds.\n\nNote that the `\u003ccid\u003e` should be pined by one or more IPFS nodes so that there's at least one node available to serve it. To do so, you may run:\n\n```sh\n$ jsipfs pin add \u003ccid\u003e\n```\n\n## Interested in knowing more?\n\nThe `peer-base` library is still in its infancy. We are actively working on adding features such as Identity, Authentication and Authorization.\n\nIf you are interested in helping us or even just tracking progress, you may do so via:\n\n- IPFS's Dynamic Data and Capabilities Working Group on GitHub - https://github.com/ipfs/dynamic-data-and-capabilities\n- `#ipfs` and `#ipfs-dynamic-data` IRC channels on freenode.net\n- `peer-base` repository on GitHub - https://github.com/peer-base/peer-base\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fipfs-shipyard%2Fworkshop-todo-dapp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fipfs-shipyard%2Fworkshop-todo-dapp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fipfs-shipyard%2Fworkshop-todo-dapp/lists"}