ClickAwayListener's callback function stops executing midway

I am currently utilizing Material-UI's ClickAwayListener in conjunction with react-router for my application. The issue I have come across involves the callback function of the ClickAwayListener being interrupted midway to allow a useEffect to run, only to then resume execution afterwards. This behavior is unexpected for a callback function and it should ideally be completed before the useEffect is triggered. Below is the code snippet I have created to showcase this problem and you can view the demo here

import React, { useEffect, useState } from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useHistory,
  useParams
} from "react-router-dom";

export default function BasicExample() {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/">
            <ButtonPage />
          </Route>
          {/*Main focus route here*/ }
          <Route path="/:routeId">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// Main focus here
function Home() {
  const history = useHistory();
  const { routeId } = useParams();

  const [count, setCount] = useState(0);

  const handleClick1 = () => {
    history.push("/route1");
  };
  const handleClick2 = () => {
    history.push("/route2");
  };

  // useEffect run on re-render and re-mount
  useEffect(() => {
    console.log("Re-render or remount");
  });
  
  useEffect(() => {
    console.log("Run on route change from inside useEffect");
  }, [routeId]);

  const handleClickAway = () => {
    console.log("First line in handle click away");
    setCount(count + 1);
    console.log("Second line in handle click away");
  };

  return (
    <div>
      <button onClick={handleClick1}>Route 1 </button>
      <button onClick={handleClick2}>Route 2 </button>
      <ClickAwayListener onClickAway={handleClickAway}>
        <div style={{ height: 100, width: 100, backgroundColor: "green" }}>
          Hello here
        </div>
      </ClickAwayListener>
    </div>
  );
}

// Just a component such that home route can navigate
// Not important for question
function ButtonPage() {
  const history = useHistory();

  const handleClick1 = () => {
    history.push("/route1");
  };
  const handleClick2 = () => {
    history.push("/route2");
  };

  return (
    <div>
      <button onClick={handleClick1}>Route 1 </button>
      <button onClick={handleClick2}>Route 2 </button>
    </div>
  );
}

Specifically, when I click outside the ClickAwayListener, the handleClickAway behaves as expected, with the following logging message:

First line in handle click away
Second line in handle click away
Re-render or remount 

However, if I choose to click on a button that navigates to another route, a strange scenario unfolds: handleClickAway stops at the first logging line, allowing the useEffect to run its logging, before eventually resuming where it left off. Here is the logging sequence in this case:

First line in handle click away
Re-render or remount
Run on route change from inside useEffect
Second line in handle click away

Through various testing, I have determined that the presence of setCount within the handleClickAway function is what triggers this unexpected behavior. Removing this line ensures that handleClickAway functions correctly in all scenarios. It seems that modifying the component state, causing a rerender, within handleClickAway alongside route navigation can lead to this bug.

This behavior is puzzling, given that traditional callbacks are not supposed to halt midway through execution like this. My theory is that the ClickAwayListener may somehow convert handleClickAway into a Promise, though even then, it does not justify pausing at the setCount step to allow useEffect to intervene. Can anyone provide insight into this phenomenon?

Edit 1: Upon reviewing @Rex Pan's response, it appears that the handleClickAway may contain an embedded useEffect. This deduction stems from analyzing the trace stack. However, this peculiar behavior solely manifests when navigating to a different route by clicking on a link. Clicking elsewhere does not trigger this anomaly. Can someone shed light on why and how this occurs?

Edit 2: Building upon @Rex Pan's explanation, further investigation reveals that his reasoning applies specifically to the ClickAwayListener callback. Consequently, the code snippet below showcases a similar "bug" occurring:

export default function ClickAway() {
  const classes = useStyles();
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    console.log("Test in useEffect");
  });

  const handleClickAway = () => {
    console.log("Count is " + count);
    setCount(count + 1);
    setCount(count + 10);
    console.log("Count is " + count);
  };

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <div className={classes.root}>Click away</div>
    </ClickAwayListener>
  );
}

Interestingly, this explanation does not apply universally, as evidenced by the following code snippet:

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("Run inside useEffect");
  });

  const handleClick = () => {
    console.log("First line");
    setCount(count + 1);
    console.log("Second line");
    setCount(count + 100);
    console.log("Third line");
  };

  return (
    <>
      <div>Count is: {count} </div>
      <button onClick={handleClick}>Click</button>
    </>
  );
}

I have attempted to dig into the source code of ClickAwayListner but so far, I have been unable to pinpoint the specific location responsible for this behavior. Any insights would be greatly appreciated.

Answer №1

Executing the setCount() function triggers a render that flushes any previous instances of the useEffect(). This results in the useEffect() functions being called before the setCount() function returns to the handleClickAway().

handleClickAway()
    console.log("First line in handle click away");
    setCount(count + 1);
        ...
            effect0000000()
                console.log("Re-render or remount");
            effect_on_routeId()
                console.log("Run on route change");
    console.log("Second line in handle click away");

This can be verified by including the function names and new Error() to check the callstack.

useEffect(function effect0000000() {
    console.log(new Error())
    console.log("Re-render or remount");
  });

  useEffect(function effect_on_routeId() {
    console.log(new Error())
    console.log("Run on route change");
  }, [routeId]);

  const handleClickAway = function handleClickAway() {
    console.log("First line in handle click away");
    setCount(count + 1);
    console.log("Second line in handle click away");
  };

https://i.stack.imgur.com/3MpZb.png

If you click outside,

  1. The handleClickAway() is invoked, logging First line in handle click away
  2. The setCount() is triggered, causing React to re-render with 2, scheduling the useEffect() bound to 2
  3. Home() returns, followed by setCount()
  4. handleClickAway() logs Second line in handle click away
  5. The scheduled useEffect() bound to 2 is called

https://i.stack.imgur.com/VB1Jk.png

When the route button is clicked,

  1. history.push() triggers a render of 2, scheduling the useEffect() bound to this = 2
  2. handleClickAway() is called and logs First line in handle click away
  3. The setCount() is invoked
  4. React needs to flush the previous effect (bound to this = 2) before the next render
  5. React calls Home() to render 3, scheduling the useEffect() bound to 3
  6. Home() returns, followed by setCount()
  7. handleClickAway() logs Second line in handle click away
  8. The scheduled useEffect() bound to 3 is called

https://i.stack.imgur.com/BodLs.png

Answer №2

Let me explain why this situation is occurring. The useEffect function has routeId as one of its arguments. It seems that when handleClickAway is triggered, you are probably navigating, which then activates the useEffect and logs a message in your console.

Furthermore, have you confirmed that it is actually halting the execution, or is it just logging asynchronously in between operations? Typically, useEffect functions do not block the flow of any other code.

An easy way to confirm this would be to delay the logging within the useEffect. If it doesn't block the execution, your other log messages should appear first before this delayed one shows up later.

useEffect(() => {
   setTimeout(() => console.log('delayed log'), 3000)
}, [routeId])

In response to your query:

As per information from this article, any asynchronous call operates outside the main call stack, hence it does not impede the progress of your code's execution.

To summarize, Javascript handles asynchronous tasks through a call stack, callback queue, Web API, and event loop.

React likely follows a similar approach with useEffect, ensuring that it does not halt the execution of your callback but instead runs alongside it.

The provided article also demonstrates a timeout example similar to mine and explains how it interacts with the call stack:

When dealing with Javascript, all instructions are placed on a call stack. Upon encountering a setTimeout function, the engine recognizes it as a Web API instruction and transfers it out to the Web API. Once the Web API completes the task, it gets added to the callback queue.

Answer №3

When clicking on a route button, two events trigger simultaneously creating a race condition between history.push() and setCount() functions within the respective handlers. As a result, two independent rerenders occur, each firing off an effect. This behavior is not a bug but rather a result of both handler functions executing their tasks concurrently, running all the code inside them at the same time.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issues with Angular toggle sorting functionality not functioning as expected

$scope.searchObject = { from: 0, hydrate: false, size: 12, sort: 'timestamp:desc' }; $scope.sort = function(a) { var ascend = a + ':' + 'asc'; var descend = a + ':' + 'desc'; if ($scope.searc ...

Trouble with Google Interactive Charts failing to load after UpdatePanel refresh

Desperately seeking assistance! I have spent countless hours researching this issue but have hit a dead end. My dilemma involves using the Google Interactive Charts API within an UpdatePanel to dynamically update data based on dropdown selection changes. H ...

Placing a JavaScript button directly below the content it interacts with

I am currently working on a button that expands a div and displays content upon clicking. I am facing an issue where I want the button to always be positioned at the bottom of the div, instead of at the top as it is now, but moving it within the parent div ...

Tips for transitioning from custom CSS to Material UI's CSS in JS

I came across a project where someone implemented components with custom CSS. One interesting thing I noticed was a wrapper component, similar to Material UI's Container or just a simple div with applied styles. export const Container = styled.div` ...

Displaying content on a click event in AngularJS

I am facing an issue with the ng-show directive not working as expected when a user clicks on an icon. The desired behavior is that the parent div should initially display content, but when the play icon is clicked, the contents of the div should become ...

Neither Output nor EventEmitter are transmitting data

I am struggling to pass data from my child component to my parent component using the Output() and EventEmitter methods. Despite the fact that the emitter function in the child component is being called, it seems like no data is actually being sent through ...

Data is not being successfully transmitted through the ORM (Prisma) to the database

After spending hours trying to solve this issue, I've hit a roadblock. My current project is using Next.js 13. I managed to successfully connect my application to a Postgres Database, configure the Prisma schema, and test a migration with a seed.ts f ...

Using D3.js to plot data points on a topojson map's coordinates

Having difficulty converting latitude and longitude coordinates to "cx" and "cy" positions on my SVG map created with d3 and topojson. Despite researching solutions online, I am unable to successfully implement the conversion process. Each time I try to co ...

SwitchBase is undergoing a transformation where its unchecked state goes from uncontrolled to controlled. It is important to note that elements should not transition back and forth between un

There is a MUI checkbox I am using: <Checkbox checked={rowValues[row.id]} onChange={() => { const temp = { ...rowValues, [row.id]: !rowValues[row.id] }; setRowValues(temp); }} in ...

Tips for accessing and adjusting an ngModel that is populated by an attribute assigned via ngFor

Looking for guidance on how to modify an input with ngModel attribute derived from ngFor, and update its value in the component. Here is my code snippet for reference: HTML FRONT The goal here is to adjust [(ngModel)] = "item.days" based on button click ...

What repercussions come from failing to implement an event handler for 'data' events in post requests?

If you take a look at the response provided by Casey Chu (posted on Nov30'10) in this particular question: How do you extract POST data in Node.js? You'll find that he is handling 'data' events to assemble the request body. The code sn ...

Tapping into the space outside the MUI Modal Component in a React application

Can Modal Component in MUI be used with a chat bot? When the modal is open, can users interact with buttons and inputs outside of it? How can this be implemented? I want to be able to click outside the modal without closing it. Modal open={open} onClo ...

Tips on creating a post that can be viewed exclusively by one or two specific countries

I'm stumped on how to create a post that is visible only to specific countries. How can I code it to determine the user's country without requiring them to make an account? Any advice or hints would be greatly appreciated. ...

Navigating a double entry validation in a Java script prompt while utilizing Selenium

Can someone please assist me with the following scenario: I need to complete a double entry check prompt/alert that contains two text boxes. The task is to fill in these two text boxes and then click on the OK button. Potential solutions attempted: I tr ...

Display the variance in values present in an array using Node.js

I am facing a challenge and need help with printing arrays that contain both same and different values. I want to compare two arrays and print the values that are present in both arrays in one array, while also creating another array for the differing val ...

How to retrieve the value from an editable td within a table using Jquery

I am working with a dynamic table that looks like this: <table> <tbody> <tr> <td>1</td> <td contenteditable='true'>Value1</td> </tr> <tr> ...

ReactJS universal-cookie package experiencing issue with get() method

I am facing an issue with the interaction between two components stored in separate .js files. import Cookie from 'universal-cookie' class Comp1{ someFunction(){ // Call google Oauth // get userinfo from Oauth response and set the ...

A windows application developed using either Javascript or Typescript

Can you provide some suggestions for developing Windows applications using Javascript, Typescript, and Node.js? ...

Dynamic imports in ViteJS do not support TS-React

Here's how I am calling the Function <DynamicIcon name={"Money"}/> The DynamicIcon function implementation import React, {lazy, Suspense} from 'react'; export default function DynamicIcon(props: { name: string }) { cons ...

Discover the magic of Google Charts with the ability to showcase HTML source code within tooltips

I am attempting to show a Pie Chart using Google Charts with HTML in the tooltips, but I am encountering an issue where the HTML source is visible. I have tried setting html:true on the data column and tooltip.isHtml in the options, but I am unsure of what ...