nimeshnayaju / y-presence

Simple react hooks to manage multiplayer presence using Yjs
MIT License
176 stars 4 forks source link

Issue when using with tiptap collaboration cursor #7

Open seleckis opened 6 months ago

seleckis commented 6 months ago

useSelf works well in most situations. But when I use it with TipTap editor and CollaborationCursor extension here comes an issue:

In a component that renders tipTap editor I need to update the presence of the user like this:

const self = useSelf();
useEffect(() => {
  if (!self || !editor) return;
  editor.commands.updateUser({
    name: self.username,
    color: self.color,
  });
}, [self, editor]);

Unfortunately this code makes tiptap editor opened websocket to go mad and infinity send a huge amount of messages.

I have create this hook:

import { isEqual } from "lodash-es";
import { useSelf as usePresenceSelf } from "y-presence";

export const useSelf = () => {
  const self = usePresenceSelf(awareness) as YUser;
  const selfRef = useRef(self);
  useEffect(() => {
    if (isEqual(self, selfRef.current)) return;
    selfRef.current = self;
  }, [self]);
  return selfRef.current;
};

Now update is working as expected and there is no infinite loop of messages anymore.

Should this be added in the y-presence lib? What do you think?

nimeshnayaju commented 6 months ago

In a component that renders tipTap editor I need to update the presence of the user like this:

const self = useSelf();
useEffect(() => {
  if (!self || !editor) return;
  editor.commands.updateUser({
    name: self.username,
    color: self.color,
  });
}, [self, editor]);

Unfortunately this code makes tiptap editor opened websocket to go mad and infinity send a huge amount of messages.

Hi @seleckis, in this code snippet you posted, are you attempting to update the user presence every time the value of self is updated? If so, the behaviour you experienced is expected. It's almost like updating the current user's presence every time the user's presence is updated, which triggers an infinite loop of updating presence. If not, do you mind providing more context on what you are trying to achieve with the code snippet? I'd love to help if I can.

I have create this hook:

import { isEqual } from "lodash-es";
import { useSelf as usePresenceSelf } from "y-presence";

export const useSelf = () => {
  const self = usePresenceSelf(awareness) as YUser;
  const selfRef = useRef(self);
  useEffect(() => {
    if (isEqual(self, selfRef.current)) return;
    selfRef.current = self;
  }, [self]);
  return selfRef.current;
};

This is a nice way to ensure selfRef is only updated if the current user's presence is actually updated. But, there are some minor problems with this approach from what I understand. First of all, React doesn't recommend reading ref.current during rendering (Link) and second, if your goal is to perform some work when user's presence is updated, I'd recommend subscribing (and then unsubscribing during unmount) to user's awareness using the awaresness.on('update') or awareness.on('change') API (Link).

seleckis commented 6 months ago

Well, I need to set user data for CollaborationCursor extension. According to documentation it should be set when initializing editor with extensions:

const self = useSelf();
const extensions = useMemo(() => [
  Collaboration.configure({
    document: ydoc,
    fragment: yField,
  }),
  CollaborationCursor.configure({
    provider: yprovider,
    user: {
      user: self?.username,
      color: self?.color,
    },
  }),
], [yField, self]);
const editor = useEditor({
  extensions,
});

but in this case sometimes self?.username returns clientID of current user and does not update user data when useSelf from y-presence updates and returns correct user data. That is why I'm using useEffect to update user data for CollaborationCursor.

I have broken down the object useSelf gives and discovered that CollaborationCursor sets its own data, maybe that is why useSelf returns a new updated object every time.

So do you suggest to use awareness events for this case and not useSelf? Alright:

useEffect(() => {
  const onChange = ({ added, updated, removed }) => {
    console.log(added, updated, removed);
  })
  awareness.on("change", onChange);
  return () => {
    awareness.off("change", onChange);
  };
}, []);

in this case it gives me only clientID but not other data like username and color.

Alright, I've ended up with this solution:

export const useAwareness = (cb: (self: YUser | null) => void) => {
  useEffect(() => {
    const onChange = ({ updated }: { updated: number[] }) => {
      if (updated.includes(awareness.clientID)) {
        const self = awareness.getLocalState();
        cb(self as YUser);
      }
    };
    awareness.on("change", onChange);
    return () => {
      awareness.off("change", onChange);
    };
  }, [cb]);
};
...
useAwareness((self) => {
  if (!editor || !self?.username) return;

  const { username, color } = self;

  editor.commands.updateUser({
    name: username,
    color,
  });
});

This works well, updates tiptap collaboration cursor with correct data and does not force websocket to send infinite messages. I was just hoping y-presence lib could do these things, but maybe I'm wrong.