John
John

Reputation: 117

Clicking on the tabs doesn't restart the functions running inside

I'm using Materal-UI's Tabs and I'm facing a problem where every time I click on a tab, it doesn't start executing the functions from the start. Instead all the functions are done in all the tabs at the same time which is not the required thing I need. How can I change it so that when I click on that tab it starts executing the functions inside from the start?

I have made an example of what I mean in the codesandbox below showing 3 tabs with a timer of 5 secs, where all the functions in the tab are executed at the same time which is not what is desired and clicking on the tab doesn't restart it

CodeSandBox Code: https://codesandbox.io/s/material-demo-forked-18xwt

function TabPanel(props) {
  const { children, value, index, ...other } = props;

  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && (
        <Box p={3}>
          <Typography>{children}</Typography>
        </Box>
      )}
    </div>
  );
}

TabPanel.propTypes = {
  children: PropTypes.node,
  index: PropTypes.any.isRequired,
  value: PropTypes.any.isRequired
};

function a11yProps(index) {
  return {
    id: `simple-tab-${index}`,
    "aria-controls": `simple-tabpanel-${index}`
  };
}

const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1,
    backgroundColor: theme.palette.background.paper
  }
}));

export default function SimpleTabs() {
  const classes = useStyles();
  const [value, setValue] = React.useState(0);
  const [text1, setText1] = React.useState("Wait 1");
  const [text2, setText2] = React.useState("Wait 2");
  const [text3, setText3] = React.useState("Wait 3");

  const handleChange = (event, newValue) => {
    setValue(newValue);
  };

  //
  function tab1() {
    setTimeout(function () {
      setText1("Done 1");
    }, 5000);
  }

  //
  function tab2() {
    setTimeout(function () {
      setText2("Done 2");
    }, 5000);
  }

  //
  function tab3() {
    setTimeout(function () {
      setText3("Done 3");
    }, 5000);
  }

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Tabs
          value={value}
          onChange={handleChange}
          aria-label="simple tabs example"
        >
          <Tab label="Item One" {...a11yProps(0)} />
          <Tab label="Item Two" {...a11yProps(1)} />
          <Tab label="Item Three" {...a11yProps(2)} />
        </Tabs>
      </AppBar>
      <TabPanel value={value} index={0}>
        {text1}
        {tab1()}
      </TabPanel>
      <TabPanel value={value} index={1}>
        {text2}
        {tab2()}
      </TabPanel>
      <TabPanel value={value} index={2}>
        {text3}
        {tab3()}
      </TabPanel>
    </div>
  );
}

Upvotes: 1

Views: 88

Answers (1)

Drew Reese
Drew Reese

Reputation: 202751

Issues

  1. You are doing state updates as side-effect in the "render" function return, the return should be pure.
  2. All the tab panels mount at the same time, and remain mounted.
  3. You don't react to the tab index changing, so no functions are re-ran.

Solution

  1. Use an useEffect hook to "restart" callbacks.
  2. Each callback should "reset" state.
  3. Save the timeout reference so any running timers can be cleared when resetting.

Updated Component

export default function SimpleTabs() {
  const classes = useStyles();
  const [value, setValue] = useState(0);
  const [text1, setText1] = useState("Wait 1");
  const [text2, setText2] = useState("Wait 2");
  const [text3, setText3] = useState("Wait 3");

  const timer1Ref = useRef();
  const timer2Ref = useRef();
  const timer3Ref = useRef();

  const handleChange = (event, newValue) => {
    setValue(newValue);
  };

  useEffect(() => {
    switch (value) { // <-- invoke specific callback associated with tab
      case 0:
        tab1();
        break;

      case 1:
        tab2();
        break;

      case 2:
        tab3();
        break;

      default:
      // ignore
    }
  }, [value]); // <-- trigger when tab index value updates

  function tab1() {
    clearTimeout(timer1Ref.current);
    setText1("Wait 1"); // <-- reset state
    timer1Ref.current = setTimeout(function () {
      setText1("Done 1");
    }, 5000);
  }

  function tab2() {
    clearTimeout(timer2Ref.current);
    setText2("Wait 2");
    timer2Ref.current = setTimeout(function () {
      setText2("Done 2");
    }, 5000);
  }

  function tab3() {
    clearTimeout(timer3Ref.current);
    setText3("Wait 3");
    timer3Ref.current = setTimeout(function () {
      setText3("Done 3");
    }, 5000);
  }

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Tabs
          value={value}
          onChange={handleChange}
          aria-label="simple tabs example"
        >
          <Tab label="Item One" {...a11yProps(0)} />
          <Tab label="Item Two" {...a11yProps(1)} />
          <Tab label="Item Three" {...a11yProps(2)} />
        </Tabs>
      </AppBar>
      <TabPanel value={value} index={0}>
        {text1}
        {/* {tab1()} */} // <-- Don't update state from return!!
      </TabPanel>
      <TabPanel value={value} index={1}>
        {text2}
        {/* {tab2()} */}
      </TabPanel>
      <TabPanel value={value} index={2}>
        {text3}
        {/* {tab3()} */}
      </TabPanel>
    </div>
  );
}

Edit clicking-on-the-tabs-doesnt-restart-the-functions-running-inside

Note: This is a lot of really repetitive code, i.e. not very DRY. Try to abstract the common behavior into a single function.

Upvotes: 2

Related Questions