Wednesday 2 December 2020

Callback function for ClickAwayListener stop midway on execution

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=>
          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?



from Callback function for ClickAwayListener stop midway on execution

No comments:

Post a Comment