Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collaborative text field using react with debounce #614

Open
giannissavvidis opened this issue Jul 7, 2023 · 3 comments
Open

Collaborative text field using react with debounce #614

giannissavvidis opened this issue Jul 7, 2023 · 3 comments

Comments

@giannissavvidis
Copy link

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.

@alecgibson
Copy link
Collaborator

I have to say we used to debounce, but we actually stopped because it's a much smoother collaboration experience without the debounce (since edits arrive at remote users' editors as fast as possible).

If you don't need to go back to previous documents, you can TTL your ops collection.

@curran
Copy link
Contributor

curran commented Jul 7, 2023

I believe ShareDB debounces based on network latency already, internally, so no need to worry about manually batching.

@curran
Copy link
Contributor

curran commented Jul 7, 2023

This would be awesome to have as a new example in the examples folder!

Also JFYI, if you use json1 there is no need to do the cloning as it uses immutable update patterns internally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants