Home > Enterprise >  Avoiding race conditions with react hooks and native callbacks
Avoiding race conditions with react hooks and native callbacks

Time:12-18

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.

CodePudding user response:

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.

CodePudding user response:

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.

  • Related