skip to Main Content

I’m trying to write some inter-frame-comunication hook and I’m not sure that the implementation is correct. Unfortunately, the react lifecycle topic seems very complex (example) and I couldn’t find a definite answer or recommendation about how to implement it correctly.

Here’s my attempt at writing the hook:

const frame = /*...*/;
let messageId = 0;

function usePostMessage(
  eventName: string,
  handler: (success: boolean) => void
) {
  const [pendingMessageId, setPendingMessageId] = useState<number>();

  const postMessage = useCallback(() => {
    frame.postMessage(eventName);
    setPendingMessageId(++messageId);
  }, [eventName]);

  useEvent(
    "message",
    useCallback(
      (message) => {
        if (
          message.eventName === eventName &&
          message.messageId === pendingMessageId
        ) {
          handler(message.success);
          setPendingMessageId(undefined);
        }
      },
      [eventName, handler, pendingMessageId]
    )
  );

  return { postMessage, pendingMessageId };
}

(I’m using useEvent)

Usage:

const { postMessage, pendingMessageId } = usePostMessage(
  "eventName",
  (success) => {
    console.log("eventName", success ? "succeeded" : "failed");
  }
);

if (pendingMessageId !== undefined) {
  return <div>Pending...</div>;
}

return <button onclick={postMessage}>Click me</button>;

As you can see, I tried to implement a way to post a message and get a response from a frame. I also tried to avoid pitfalls such as getting unrelated responses by keeping a message counter.

It works, but I’m afraid that the "message" event might arrive before the setPendingMessageId state is updated. Is that possible? Are there any guidelines or best practices for implementing this correctly? Thanks.

2

Answers


  1. Update the setPendingMessageId inside the useEffect hook

    useEffect(() => {
        setPendingMessageId(++messageId);
      }, [postMessage])
    

    state update is applied after the postMessage function has been called, avoiding the race condition.

    Login or Signup to reply.
  2. I’m afraid that the "message" event might arrive before the setPendingMessageId state is updated. Is that possible?

    No. If a state setter is called inside a React function (such as an onclick prop, as in your code), React will re-render a component after that React handler finishes running its code. JavaScript is single-threaded; once setPendingMessageId(++messageId); is called, the click handler will end, and then a re-render will occur. There’s no chance of any other code running before then. The receipt of the message goes through a non-React API (the message listener on the window), so React doesn’t try to integrate it into the rerendering flow.

    That said, although your code will work, to avoid having to worry about this, some might prefer to reference the stateful values as they are when the message is posted rather than put the logic in a separate hook, which could be less reliable if the state gets out of sync for some other reason. So instead of useEvent, you could consider something along the lines of

    const postMessage = useCallback(() => {
      frame.postMessage(eventName);
      setPendingMessageId(++messageId);
      // Save a reference to the current value
      // just in case it changes before the response
      const thisMessageId = messageId;
      const handler = ({ data }) => {
        if (data.eventName === eventName && data.messageId === thisMessageId) {
          handler(data);
          window.removeEventListener('message', handler);
        }
      };
      window.addEventListener('message', handler);
    }, [eventName]);
    

    Having a messageId outside of React is a little bit smelly. It’d be nice if you could integrate it into state somehow (perhaps in an ancestor component) and then add it to the dependency array for postMessage.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search