Skip to content

Collaborative text field using react with debounce #614

@giannissavvidis

Description

@giannissavvidis

First of all thanks for this awesome package!

I wanted to create a collaborative text field. The closest one I could find was the textarea example located in /examples/textarea and is using a third party package called sharedb-string-binding. However it is using plain html with javascript, so by looking at the code I tried to recreate it using react (but could be done using angular/vue/svelte).

import { Stack, TextField } from "@mui/material";
import ReconnectingWebSocket from "reconnecting-websocket";
import { useMount, useUnmount } from "react-use";
import { useEffect, useState } from "react";
import { Connection } from "sharedb/lib/client";
import { cloneDeep } from "lodash";
import jsondiff from "json0-ot-diff";
import diffMatchPatch from "diff-match-patch";

const App = () => {
  const [socket, setSocket] = useState(null);
  const [doc, setDoc] = useState(null);
  const [docData, setDocData] = useState(null);

  useMount(() => {
    const socket = new ReconnectingWebSocket(
      process.env.REACT_APP_SOCKER_URL,
      [],
      {
        maxRetries: 3,
      }
    ); // connect to the socket

    setSocket(socket); // save the socket instance

    const connection = new Connection(socket); // create a connection

    const document = connection.get("test-collection", "123"); // get the document

    setDoc(document); // save the document to state

    document.subscribe((e) => {
      // subscribe to the document
      if (e) {
        console.error(e);
        return;
      }

      // If document.type is undefined, the document has not been created, so let's create it
      if (!document.type) {
        document.create(
          {
            collaborativeField: "",
          },
          (e) => {
            if (e) {
              console.log(e);
            }
          }
        );
      }

      setDocData(cloneDeep(document.data)); // save document data to state, cloneDeep is mandatory
    });
  });

  useUnmount(() => {
    socket?.close();
    doc?.unsubscribe();

    setDocData(null);
  });

  useEffect(() => {
    if (!doc || !docData) return;

    // setup the onOp listener.

    const onOp = (_e, source) => {
      if (source) return; // if source is true, meaning its acknowledging user's own operations, do nothing
      setDocData(cloneDeep(doc.data)); // else save the updated document data to state
    };

    doc.on("op", onOp);

    return () => doc.removeListener("op", onOp);
  }, [doc, docData]);

  const handleInputChange = (e) => {
    let newValue = e.target.value; // the new value

    const newDocData = cloneDeep(docData); // create a copy of the document data in order to modify it locally in the function

    newDocData.collaborativeField = newValue; // update the field with the new value

    const diff = jsondiff(docData, newDocData, diffMatchPatch);
    // use the jsondiff package to generate the json0 operations ...
    // to go from the docData to newDocData...
    // use diffMatchPatch as third argument because our field is a string

    console.log(diff);

    setDocData(newDocData); // set the new docData to state because we do not acknowledging our own operations

    doc.submitOp(diff); // submit the operation
  };

  return (
    <Stack
      sx={{ height: "100vh", alignItems: "center", justifyContent: "center" }}
    >
      {docData && ( // render if docData is initialized
        <TextField
          label="Collaborative text field"
          variant="outlined"
          size="small"
          value={docData.collaborativeField}
          onChange={handleInputChange}
        />
      )}
    </Stack>
  );
};

export default App;

feel free to comment at any part of my implementation as I would like to make a PR to provide an example using any of the modern front-end frameworks. However the main issue is if it is possible to add a debounce in order to significantly reduce the number of operations that are submitted. Both in my implementation and in the provided textarea example, a new operation is submitted on every keypress meaning that a new document is created in the history collection. Imagine we have an application with many collaborators and many collaborative fields. The history will become enormous quite fast. A debouncer will significantly reduce the size of the history collection but even with a debouncer in place is there a recommended way of handling the history?

Thanks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions