Multi-User Todo App Using SyncState
Published: 2020-12-21 12:28:05

Hi! This article is divided into two parts. Firstly, we will build a Todo app using React and SyncState. Next, we will add a multi-user (shared state) functionality with the help of the remote-plugin of SyncState that works with socket.io.

If you don't already know, SyncState is a general-purpose **state-management library** for React & JavaScript apps.It can be used for local states and also makes it easy to sync the state across multiple sessions without learning any new APIs.You can read more about it in our [official documentation](https://syncstate.github.io/?utm_source=Sync%20State&utm_medium=Dev.to&utm_campaign=Tutorial%20Article).

You can check out the complete project on [Github](https://github.com/syncstate/multi-user-todo-example?utm_source=Sync%20State&utm_medium=Dev.to&utm_campaign=Tutorial%20Article).

## Part 1: Building Todo app with SyncState

### Starting the project using Create React App

Let's initialise a basic React app. Make sure that you have node and npm pre-installed.

```bash
npx create react-app sync-multi-user-todo
```

Next, navigate to the project directory:

```bash
cd sync-multi-user-todo
```

Run the project:

```bash
npm run start
```

Navigate to `localhost:3000` in your browser.

Your application has now been set-up and you can move on to building the rest of the app.

### Styling Your Application

 We will use Bootstrap and FontAwesome for nice user interface.

In order to use them, put this in the head section of your **public/index.html** file:

```html
 <link
   rel="stylesheet"
   href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
 />
 <link
   rel="stylesheet"
   href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
   integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
   crossorigin="anonymous"
 />
```

Open App.css and replace the contents with the following:

```css
body {
  background-color: #3f937b;
}
.App {
  text-align: center;
}

.checkbox {
  margin-left: -25px;
}
.caption {
  width: 300px;
}
.addText { 
  width: 350px;
}
button {
  background-color: #61b9a0 !important;
  color: white !important;
}
.btn:focus,
.btn:active {
  outline: none !important;
  box-shadow: none;
  border-color: #61b9a0 !important;
}
.input-todo:focus,
.input-todo:active {
  outline: none !important;
  box-shadow: none !important;
  border-color: #61b9a0;
}

@media (max-width: 375px) {
  .checkbox {
    margin-left: -35px;
  }
  .caption {
    width: 280px;
  }

  .addText {
    width: 330px;
  }
}

@media (max-width: 320px) {
  .caption {
    width: 180px;
  }

  .addText {
    width: 230px;
  }
}

todoTitle {
  width: "89%";
}
```

### Installing SyncState

Now, let's add `syncstate` to our react application. Open the terminal and execute the following commands:

```bash
npm install @syncstate/core
npm install @syncstate/react
```

SyncState provides **createDocStore** and **Provide**r

```jsx
import { createDocStore } from "@syncstate/core";
import { Provider } from "@syncstate/react";
```

Import createDocStore and Provider in your index.js file.

### **Creating the store**

 SyncState maintains a universal store for your application. In this store, all your data is contained in a single document. SyncState uses JSON patches to update the document.

Since we are building a multi-user todo app, we'll need an empty array as our state.

In your index.js file create a store as follows:

```jsx
const store = createDocStore({ todos: [] });
```

Wrap your app with Provider and pass the store prop:

```jsx
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
```

Replace the content of your App.js file with the following code:

```jsx
import React from "react";
import "./App.css";

function App() {

  return (
    <div className="container mt-5">
      <h2 className="text-center text-white">
        Multi User Todo Using SyncState
      </h2>
      <div className="row justify-content-center mt-5">
        <div className="col-md-8">
          <div className="card-hover-shadow-2x mb-3 card">
            <div className="card-header-tab card-header">
              <div className="card-header-title font-size-lg text-capitalize font-weight-normal">
                <i className="fa fa-tasks"></i>&nbsp;Task Lists
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

Let's begin with the CRUD part of our application.

### Reading To-Do Items

You can access the store in your components using the **useDoc** hook:

```jsx
import { useDoc } from "@syncstate/react";
```

useDoc hook accepts a path parameter, it returns the state at the path and the function to modify that state. This also adds a listener to the path and updates the component when the state at the path changes.

SyncState uses a pull-based strategy i.e. only those components listen to changes if they are using the data that gets changed in your store.

To get the hold of our todo array, we'll pass the path **"/todos"** to the useDoc hook:

```jsx
 const todoPath = "/todos";
 const [todos, setTodos] = useDoc(todoPath);
```

and to get the hold of a todo item we'll pass the path **"/todos/{index}**" to the useDoc hook:

```jsx
const todoItemPath = "/todos/1";
//todoItem at index 1
const [todoItem, setTodoItem] = useDoc(todoItemPath);
```

For performance reasons, it's recommended to make the path as specific as possible. A thumb rule is to fetch only the slice of doc that you have to read or modify.

Moving forward, in App component use **useDoc** hook to get the todos.You can now use todos array anywhere in your component. 😁

```jsx

import React from "react";
import "./App.css";
import { useDoc } from "@syncstate/react";

function App() {
 
  const todoPath = "/todos";
  const [todos, setTodos] = useDoc(todoPath);
  
  return (
    <div className="container mt-5">
      <h2 className="text-center text-white">
        Multi User Todo Using SyncState
      </h2>
      <div className="row justify-content-center mt-5">
        <div className="col-md-8">
          <div className="card-hover-shadow-2x mb-3 card">
            <div className="card-header-tab card-header">
              <div className="card-header-title font-size-lg text-capitalize font-weight-normal">
                <i className="fa fa-tasks"></i>&nbsp;Task Lists
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

Now, we'll create a component **TodoItem** which will take the path (todoItemPath) of each todo item as a prop and will show the content of the todo item.

Create a new folder named component in src directory and add a new file TodoItem.js containing the following code:

```jsx
import React from "react";
import { useDoc } from "@syncstate/react";

function TodoItem({ todoItemPath }) {
  
  return (
    <div>
      <div className="d-flex align-content-center">
        <div
          className="d-flex align-items-center todoTitle"
        >
          <div style={{ width: "100%" }}>{todoItem.caption} </div>
        </div> 
      </div>
    </div>
  );
}

export default TodoItem;
```

Revisit App.js and create a new array of items by mapping over the todo items from state and pass each todo's path as a prop to our TodoItem component:

```jsx
import React from "react";
import "./App.css";
import TodoItem from "./components/TodoItem";
import { useDoc } from "@syncstate/react";

function App() {
  const todoPath = "/todos";
  const [todos, setTodos] = useDoc(todoPath);

  const todoList = todos.map((todoItem, index) => {
    return (
      <li key={todoItem.index} className="list-group-item">
        <TodoItem todo={todoItem} todoItemPath={todoPath + "/" + index} />
      </li>
    );
  });

  return (
    <div className="container mt-5">
      <h2 className="text-center text-white">
        Multi User Todo Using SyncState
      </h2>
      <div className="row justify-content-center mt-5">
        <div className="col-md-8">
          <div className="card-hover-shadow-2x mb-3 card">
            <div className="card-header-tab card-header">
              <div className="card-header-title font-size-lg text-capitalize font-weight-normal">
                <i className="fa fa-tasks"></i>&nbsp;Task Lists
              </div>
            </div>
            <div
              className="overflow-auto"
              style={{ height: "auto", maxHeight: "300px" }}
            >
              <div className="position-static">
                <ul className=" list-group list-group-flush">{todoList}</ul>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

### Creating Todo Items

Now, let’s give our app the power to create a new item.

Let’s build the **addTodo** function in the App component. Basically, the addTodo function will receive a todoItem and add it to our todos state.

In our App.js add the addTodo function:

```jsx
//generate unique id
  const keyGenerator = () => "_" + Math.random().toString(36).substr(2, 9);
  const addTodo = (todoItem) => {
    setTodos((todos) => {
      let id = keyGenerator();
      todos.push({
        id: id,
        caption: todoItem,
        completed: false,
      });
      document.getElementsByClassName("input-todo")[0].value = "";
    });
  };
```

It may seem like we are mutating our todos state directly in the setTodo function but we are updating the state using Immer.js which generates JSON patches for our internal reducers.

Now, add another component named AddTodo to the components folder. AddTodo component gives us the functionality of sending the input to the addTodo function.

```jsx
import React, { useState } from "react";

function AddTodo({ addTodo }) {
  const [input, setInput] = useState("");

  return (
    <div
      className="d-block text-right card-footer d-flex"
      style={{ padding: "0.75rem" }}
    >
      <div className=" position-relative col " style={{ paddingLeft: "13px" }}>
        <input
          type="text"
          className="form-control input-todo"
          value={input}
          onChange={(e) => {
            setInput(e.target.value);
          }}
          onKeyPress={(event) => {
            if (event.which === 13 || event.keyCode === 13) {
              addTodo(input);
              setInput("");
            }
          }}
          placeholder="Enter new todo"
        />

        <i
          className="fa fa-close"
          style={{
            position: "absolute",
            top: "25%",
            right: "25px",
          }}
          onClick={() => setInput("")}
        ></i>
      </div>
      <div className="ml-auto">
        <button
          type="button"
          className="border-0 btn-transition btn btn-outline-danger"
          onClick={(e) => {
            e.preventDefault();
            addTodo(input);
            setInput("");
          }}
        >
          Add Task
        </button>
      </div>
    </div>
  );
}

export default AddTodo;
```

So far, the src/App.js file looks like this:

```jsx
import React from "react";
import "./App.css";
import TodoItem from "./components/TodoItem";
import AddTodo from "./components/AddTodo";
import { useDoc } from "@syncstate/react";

function App() {
  const todoPath = "/todos";
  const [todos, setTodos] = useDoc(todoPath);

  //generate unique id
  const keyGenerator = () => "_" + Math.random().toString(36).substr(2, 9);
  const addTodo = (todoItem) => {
    setTodos((todos) => {
      let id = keyGenerator();
      todos.push({
        id: id,
        caption: todoItem,
        completed: false,
      });
      document.getElementsByClassName("input-todo")[0].value = "";
    });
  };

  const todoList = todos.map((todoItem, index) => {
    return (
      <li key={todoItem.index} className="list-group-item">
        <TodoItem todo={todoItem} todoItemPath={todoPath + "/" + index} />
      </li>
    );
  });

  return (
    <div className="container mt-5">
      <h2 className="text-center text-white">
        Multi User Todo Using SyncState
      </h2>
      <div className="row justify-content-center mt-5">
        <div className="col-md-8">
          <div className="card-hover-shadow-2x mb-3 card">
            <div className="card-header-tab card-header">
              <div className="card-header-title font-size-lg text-capitalize font-weight-normal">
                <i className="fa fa-tasks"></i>&nbsp;Task Lists
              </div>
            </div>
            <div
              className="overflow-auto"
              style={{ height: "auto", maxHeight: "300px" }}
            >
              <div className="position-static">
                <ul className=" list-group list-group-flush">{todoList}</ul>
              </div>
            </div>
            <AddTodo addTodo={addTodo} />
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

#### We're now able to add and view todo items. 😄

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8dwr06dr2hp3u5dpi88z.png)

### Updating To-Do Items

Let’s add the functionality to cross off an item on your to-do list when they are completed.

Add **toggleTodo** function and a button to toggle completed property of todoItem to true or false:

```jsx
import React from "react";
import { useDoc } from "@syncstate/react";

function TodoItem({ todoItemPath }) {
  const [todos, setTodos] = useDoc("/todos", Infinity);
  const [todoItem, setTodoItem] = useDoc(todoItemPath);

 
  const toggleTodo = (completed) => {
    setTodoItem((todoItem) => {
      todoItem.completed = completed;
    });
  };

 const getTxtStyle = {
    textDecoration: todoItem.completed ? "line-through" : "none",
    marginLeft: "10px",
  };

  return (
    <div>
      <div className="d-flex align-content-center">
        <div
          className="custom-checkbox custom-control d-flex align-items-center"
          style={{ marginBottom: "2px" }}
        >
          <input
            type="checkbox"
            className="form-check-input"
            checked={todoItem.completed}
            onChange={(e) => {
              toggleTodo(e.target.checked);
            }}
          />
        </div>

        <div
          className="d-flex align-items-center todoTitle"
          style={getTxtStyle}
        >
          <div style={{ width: "100%" }}>{todoItem.caption} </div>
        </div>
      </div>
    </div>
  );
}

export default TodoItem;
```

### Deleting Todo Items

Let’s add the functionality to delete an item from your todo list.

Add **deleteTodo** function and a button to delete todoItem:

```jsx
import React from "react";
import { useDoc } from "@syncstate/react";

function TodoItem({ todoItemPath }) {
  const [todos, setTodos] = useDoc("/todos", Infinity);
  const [todoItem, setTodoItem] = useDoc(todoItemPath);

 const deleteTodo = (id) => {
    let index;
    for (let i = 0; i < todos.length; i++) {
      if (todos[i].id === id) {
        index = i;
        break;
      }
    }
    setTodos((todos) => {
      todos.splice(index, 1);
    });
  };
  const toggleTodo = (completed) => {
    setTodoItem((todoItem) => {
      todoItem.completed = completed;
    });
  };

  const getTxtStyle = () => {
    return {
      textDecoration: todoItem.completed ? "line-through" : "none",
      marginLeft: "10px",
    };
  };

  return (
    <div>
      <div className="d-flex align-content-center">
        <div
          className="custom-checkbox custom-control d-flex align-items-center"
          style={{ marginBottom: "2px" }}
        >
          <input
            type="checkbox"
            className="form-check-input"
            checked={todoItem.completed}
            onChange={(e) => {
              toggleTodo(e.target.checked);
            }}
          />
        </div>

        <div
          className="d-flex align-items-center todoTitle"
          style={getTxtStyle()}
        >
          <div style={{ width: "100%" }}>{todoItem.caption} </div>
        </div>
        <div className="ml-auto d-flex align-items-center">
          <button
            className="border-0 btn-transition btn btn-outline-danger"
            onClick={() => {
              deleteTodo(todoItem.id);
            }}
          >
            <i className="fa fa-trash"></i>
          </button>
        </div>
      </div>
    </div>
  );
}

export default TodoItem;
```

In **deleteTodo** function, we are finding the index of the todoItem we want to delete and then splicing the todos array in **setTodo** function.

#### Congratulations, your Todo App using SyncState is complete! 😍

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/psd4xsql8nlzj4mfkhap.gif)

## Part 2: Syncing the app state with others using the remote-plugin

Remote plugin is in the experimental stage and not ready for use in production. We're working on it!

### Creating a Node server and adding Socket

We need to set up a Socket connection between client and server so that data can flow both ways. We will be using the Express.js server which will be used as our backend for Sockets.

To install Socket, execute the following commands:

```bash
npm install socket.io
npm install socket.io-client
```

Create a server  folder in root directory and create a new file index.js  in it.

Open the terminal and execute:

```bash
npm install express
```

Set up Socket for your server:

```jsx
var express = require("express");
var socket = require("socket.io");
const remote = new SyncStateRemote();
var server = app.listen(8000, function () {
  console.log("listening on port 8000");
});

var io = socket(server);

io.on("connection", async (socket) => {

});
```

Get Socket for client and set it up in your src/index.js file:

```jsx

import io from "socket.io-client";

//set up socket connection
let socket = io.connect("http://localhost:8000");
```

### Listening to the patches in the front-end and sending it over (via plugin)

Install SyncState Remote plugin for client :

```bash
npm install @syncstate/remote-client
```

Every time you make a change in your state, a JSON patch is generated from your side and is sent to the server. SyncState processes these patches and sends them to other clients.

In your src/index.js file, add the following:

```jsx
import * as remote from "@syncstate/remote-client";
```

Initialise remote by passing [remote.createInitializer()] ****as an additional argument in createDocStore function:

```jsx
const store = createDocStore({ todos: [] }, [remote.createInitializer()]);
```

Enable remote plugin on the todos path of your document tree:

```jsx
store.dispatch(remote.enableRemote("/todos"));
```

Whenever a new user joins, they should get all the patches generated till now:

```jsx
// send request to server to get patches everytime when new user joins
socket.emit("fetchDoc", "/todos");
```

If you make any changes in your Doc tree, you need to observe the changes so that you can send them to the server. 

**store.observe** observes the changes at the path and calls the listener function with the new changes/JSON patches:

```jsx
store.observe(
  "doc",
  "/todos",
  (todos, change) => {
    if (!change.origin) { 
      //send json patch to the server
      socket.emit("change", "/todos", change);
    }
  },
  Infinity
);
```

The server adds origin to the change object. If a client receives a patch from the server sent by another client, then we shouldn't send that to the server. The client should send its own patches.

After receiving the patches from server, dispatch them:

```jsx
socket.on("change", (path, patch) => {

store.dispatch(remote.applyRemote(path, patch));

});
```

The entire src/index.js file will look like this so far:

```jsx
import React from "react";
import { createDocStore } from "@syncstate/core";
import { Provider } from "@syncstate/react";
import ReactDOM from "react-dom";
import App from "./App.js";
import "./index.css";
import io from "socket.io-client";
import reportWebVitals from "./reportWebVitals";
import * as remote from "@syncstate/remote-client";

const store = createDocStore({ todos: [] }, [remote.createInitializer()]);

//enable remote plugin
store.dispatch(remote.enableRemote("/todos"));

//setting up socket connection with the server
let socket = io.connect("http://localhost:8000");

// send request to server to get patches everytime when page reloads
socket.emit("fetchDoc", "/todos");

//observe the changes in store state
store.observe(
  "doc",
  "/todos",
  (todos, change) => {
    if (!change.origin) {
      //send json patch to the server
      socket.emit("change", "/todos", change);
    }
  },
  Infinity
);

//get patches from server and dispatch
socket.on("change", (path, patch) => {
  // console.log(patch);
  store.dispatch(remote.applyRemote(path, patch));
});

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

### Consuming patches in the backend and merging it via plugin

Install SyncState Remote plugin for the server:

```bash
npm install @syncstate/remote-server
```

In your server/index.js file, create SyncState Remote instance:

```jsx
const { SyncStateRemote } = require("@syncstate/remote-server");
const remote = new SyncStateRemote();
```

We need to store the patches somewhere so that whenever a new client joins, they get all the patches. Here, we'll be using temporary storage for patches. Ideally, it should be a database.

Create a new file PatchManager.js in the server folder and add the following code:

```jsx
module.exports = class PatchManager {
  // patches;
  constructor() {
    this.projectPatchesMap = new Map();
  }

  store(projectId, path, patch) {
    // console.log("storing patch", patch, this.projectPatchesMap);
    const projectPatches = this.projectPatchesMap.get(projectId);

    if (projectPatches) {
      const pathPatches = projectPatches.get(path);

      if (pathPatches) {
        pathPatches.push(patch);
      } else {
        projectPatches.set(path, [patch]);
      }
    } else {
      const pathPatchesMap = new Map();
      pathPatchesMap.set(path, [patch]);
      this.projectPatchesMap.set(projectId, pathPatchesMap);
    }
  }

  getAllPatches(projectId, path) {
    const projectPatches = this.projectPatchesMap.get(projectId);
    if (projectPatches) {
      const pathPatches = projectPatches.get(path);

      return pathPatches ? pathPatches : [];
    }

    return [];
  }
};
```

Sending all patches to the client on joining:

```jsx
socket.on("fetchDoc", (path) => {
    //get all patches
 const patchesList = patchManager.getAllPatches(projectId, path);
  if (patchesList) {
      //send each patch to the client
      patchesList.forEach((change) => {
        socket.emit("change", path, change);
      });
    }
 });
```

Whenever a patch is received, SyncState handles conflicting updates and broadcasts to other clients:

```jsx

//patches recieved from the client
  socket.on("change", (path, change) => {
    change.origin = socket.id;

    //resolves conflicts internally
    remote.processChange(socket.id, path, change);
  });

//patches are ready to be sent
  const dispose = remote.onChangeReady(socket.id, (path, change) => {
    //store the patches in js runtime or a persistent storage
    patchManager.store(projectId, path, change);

    //broadcast the pathes to other clients
    socket.broadcast.emit("change", path, change);
  });
```

The entire server/index.js file will look like this so far:

```bash
const express = require("express");
const socket = require("socket.io");
const { v4: uuidv4 } = require("uuid");
const PatchManager = require("./PatchManager");
const { SyncStateRemote } = require("@syncstate/remote-server");
const remote = new SyncStateRemote();
const app = express();
const server = app.listen(8000, function () {
  console.log("listening on port 8000");
});

const io = socket(server);
const projectId = uuidv4();  //generate unique id 

let patchManager = new PatchManager();

io.on("connection", async (socket) => {
  socket.on("fetchDoc", (path) => {
    //get all patches
    const patchesList = patchManager.getAllPatches(projectId, path);

    if (patchesList) {
      //send each patch to the client
      patchesList.forEach((change) => {
        socket.emit("change", path, change);
      });
    }
  });

  //patches recieved from the client
  socket.on("change", (path, change) => {
    change.origin = socket.id;

    //resolves conflicts internally
    remote.processChange(socket.id, path, change);
  });
//patches are ready to be sent
  const dispose = remote.onChangeReady(socket.id, (path, change) => {
    //store the patches in js runtime or a persistent storage
    patchManager.store(projectId, path, change);

    //broadcast the pathes to other clients
    socket.broadcast.emit("change", path, change);
  });
});
```

Start the server  by executing the following command:

```bash
cd server
node index.js
```

#### Voila! Two browsers, side-by-side and in sync.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qwt4mf2ds8bq6ephts3x.gif)

[![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/47tqw1ug91t2vmi72pve.png)](https://syncstate.github.io/?utm_source=Sync%20State&utm_medium=Dev.to&utm_campaign=Tutorial%20Article)