I am using Material-UI ClickAwayListener and react-router for my app. The bug I encountered is that when the callback for ClickAwayListener get executed, it is stopped mid-way for a useEffect to run and after that it resume running. This behavior is not expected from a callback. The callback should be fully executed before the useEffect can run. Below are the code I create to demonstrate the problem and this is the demo for the code
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>
);
}
In specific, when I click outside the ClickAwayListener the handleClickAway run normally and the logging message is
First line in handle click away
Second line in handle click away
Re-render or remount
Until I choose to click on the button that navigate to other route. Here thing get weird: handleClickAway run the first logging line, then the useEffect run and print its logging, then handleClickAway resume and print its second line. So if I do so this is the logging
First line in handle click away
Re-render or remount
Run on route change from inside useEffect
Second line in handle click away
After doing some test on this bug I figured out that the thing that cause this bug is the setCount inside the handleClickAway. If I remove this line the function handleClickAway will run as expected for all cases. My conclusion is that, changing component state, or should I say, perform any actions that cause component to re-render inside handleClickAwayin combination with route navigation can cause this bug.
This behavior is strange, because as far as I know, there is no way for a normal, non-promise related callback to stop mid-way like this. I guess the ClickAwayListener somehow make handleClickAway into Promise or something. But even then, there is no reason for it to stop at the setCount and let useEffect run?
Can someone explain this to me?
Edit 1: As @Rex Pan point out in this answer, it looks like the handleClickAway has useEffect run inside it. This is the only conclusion I can make from reading the trace stack. But this behavior only happen when the user click to a link that navigate to different route. Clicking to other area beside that doesn't cause this bug. Can someone explain me why and how this happen?
Edit 2: After reading @Rex Pan answer I did some more investigation. It looks like to me that his answer is correct, but only apply for the ClickAwayListener callback. So the code below will produce the same "bug" as above
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>
);
}
However, his explanation won't work in other case, like below code
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 am trying to read on the source code of ClickAwayListner but so far haven't find the location that cause this behavior. Can someone point this out?


