title: useEffect triggers function several times with proper dependencies
body: <p>i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.</p>
  
  <pre><code>import React, { useRef, useEffect, useState, useCallback } from 'react';
  import PropTypes from 'prop-types';
  
  import { defProperty } from 'helpers';
  
  const Tabs = ({ children, value, orientation, onChange }) =&gt; {
    console.log(value);
    const indicatorRef = useRef(null);
    const tabsRef = useRef(null);
    const childrenWrapperRef = useRef(null);
  
    const valueToIndex = new Map();
    const vertical = orientation === 'vertical';
    const start = vertical ? 'top' : 'left';
    const size = vertical ? 'height' : 'width';
  
    const [mounted, setMounted] = useState(false);
    const [indicatorStyle, setIndicatorStyle] = useState({});
    const [transition, setTransition] = useState('none');
  
    const getTabsMeta = useCallback(() =&gt; {
      console.log('getTabsMeta');
      const tabsNode = tabsRef.current;
      let tabsMeta;
      if (tabsNode) {
        const rect = tabsNode.getBoundingClientRect();
  
        tabsMeta = {
          clientWidth: tabsNode.clientWidth,
          scrollLeft: tabsNode.scrollLeft,
          scrollTop: tabsNode.scrollTop,
          scrollWidth: tabsNode.scrollWidth,
          top: rect.top,
          bottom: rect.bottom,
          left: rect.left,
          right: rect.right,
        };
      }
  
      let tabMeta;
      if (tabsNode &amp;&amp; value !== false) {
        const wrapperChildren = childrenWrapperRef.current.children;
        if (wrapperChildren.length &gt; 0) {
          const tab = wrapperChildren[valueToIndex.get(value)];
          tabMeta = tab ? tab.getBoundingClientRect() : null;
        }
      }
  
      return {
        tabsMeta,
        tabMeta,
      };
    }, [value, valueToIndex]);
  
    const updateIndicatorState = useCallback(() =&gt; {
      console.log('updateIndicatorState');
      let _newIndicatorStyle;
  
      const { tabsMeta, tabMeta } = getTabsMeta();
      let startValue;
      if (tabMeta &amp;&amp; tabsMeta) {
        if (vertical) {
          startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
        } else {
          startValue = tabMeta.left - tabsMeta.left;
        }
      }
  
      const newIndicatorStyle =
        ((_newIndicatorStyle = {}),
        defProperty(_newIndicatorStyle, start, startValue),
        defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
        _newIndicatorStyle);
      if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
        setIndicatorStyle(newIndicatorStyle);
      } else {
        const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
        const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
        if (dStart &gt;= 1 || dSize &gt;= 1) {
          setIndicatorStyle(newIndicatorStyle);
          if (transition === 'none') {
            setTransition(`${[start]} 0.3s ease-in-out`);
          }
        }
      }
    }, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
  
    useEffect(() =&gt; {
      const timeout = setTimeout(() =&gt; {
        setMounted(true);
      }, 350);
      return () =&gt; {
        clearTimeout(timeout);
      };
    }, []);
  
    useEffect(() =&gt; {
      if (mounted) {
        console.log('1st call mounted');
        updateIndicatorState();
      }
    }, [mounted, updateIndicatorState]);
  
    let childIndex = 0;
    const childrenItems = React.Children.map(children, child =&gt; {
      const childValue = child.props.value === undefined ? childIndex : child.props.value;
      valueToIndex.set(childValue, childIndex);
      const selected = childValue === value;
      childIndex += 1;
  
      return React.cloneElement(child, {
        selected,
        indicator: selected &amp;&amp; !mounted,
        value: childValue,
        onChange,
      });
    });
  
    const styles = {
      [size]: `${indicatorStyle[size]}px`,
      [start]: `${indicatorStyle[start]}px`,
      transition,
    };
    console.log(styles);
    return (
      &lt;&gt;
        {value !== 2 ? (
          &lt;div className={`tabs tabs--${orientation}`} ref={tabsRef}&gt;
            &lt;span className="tab__indicator-wrapper"&gt;
              &lt;span className="tab__indicator" ref={indicatorRef} style={styles} /&gt;
            &lt;/span&gt;
            &lt;div className="tabs__wrapper" ref={childrenWrapperRef}&gt;
              {childrenItems}
            &lt;/div&gt;
          &lt;/div&gt;
        ) : null}
      &lt;/&gt;
    );
  };
  
  Tabs.defaultProps = {
    orientation: 'horizontal',
  };
  
  Tabs.propTypes = {
    children: PropTypes.node.isRequired,
    value: PropTypes.number.isRequired,
    orientation: PropTypes.oneOf(['horizontal', 'vertical']),
    onChange: PropTypes.func.isRequired,
  };
  
  export default Tabs;
  </code></pre>