Skip to content

Collaborative text field using react with debounce #614

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions