【问题标题】:useEffect triggers function several times with proper dependenciesuseEffect 多次触发具有适当依赖关系的函数
【发布时间】:2019-11-17 22:06:39
【问题描述】:

我有 Tabs 组件,它有子 Tab 组件。安装后,它会计算选项卡和选定选项卡的元数据。然后设置选项卡指示器的样式。由于某种原因,每次活动选项卡更改时,函数 updateIndicatorState 都会在 useEffect 挂钩中触发多次,并且它应该只触发一次。有人可以解释一下我在这里做错了什么吗?如果我从第二个 useEffect 钩子函数本身的 deps 中删除并添加一个 value prop 作为 dep.它只正确触发一次。但据我阅读的 react 文档 - 我不应该欺骗 useEffect 依赖数组,并且有更好的解决方案可以避免这种情况。

import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';

import { defProperty } from 'helpers';

const Tabs = ({ children, value, orientation, onChange }) => {
  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(() => {
    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 && value !== false) {
      const wrapperChildren = childrenWrapperRef.current.children;
      if (wrapperChildren.length > 0) {
        const tab = wrapperChildren[valueToIndex.get(value)];
        tabMeta = tab ? tab.getBoundingClientRect() : null;
      }
    }

    return {
      tabsMeta,
      tabMeta,
    };
  }, [value, valueToIndex]);

  const updateIndicatorState = useCallback(() => {
    console.log('updateIndicatorState');
    let _newIndicatorStyle;

    const { tabsMeta, tabMeta } = getTabsMeta();
    let startValue;
    if (tabMeta && 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 >= 1 || dSize >= 1) {
        setIndicatorStyle(newIndicatorStyle);
        if (transition === 'none') {
          setTransition(`${[start]} 0.3s ease-in-out`);
        }
      }
    }
  }, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setMounted(true);
    }, 350);
    return () => {
      clearTimeout(timeout);
    };
  }, []);

  useEffect(() => {
    if (mounted) {
      console.log('1st call mounted');
      updateIndicatorState();
    }
  }, [mounted, updateIndicatorState]);

  let childIndex = 0;
  const childrenItems = React.Children.map(children, child => {
    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 && !mounted,
      value: childValue,
      onChange,
    });
  });

  const styles = {
    [size]: `${indicatorStyle[size]}px`,
    [start]: `${indicatorStyle[start]}px`,
    transition,
  };
  console.log(styles);
  return (
    <>
      {value !== 2 ? (
        <div className={`tabs tabs--${orientation}`} ref={tabsRef}>
          <span className="tab__indicator-wrapper">
            <span className="tab__indicator" ref={indicatorRef} style={styles} />
          </span>
          <div className="tabs__wrapper" ref={childrenWrapperRef}>
            {childrenItems}
          </div>
        </div>
      ) : null}
    </>
  );
};

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;

【问题讨论】:

    标签: reactjs use-effect


    【解决方案1】:
      useEffect(() => {
        if (mounted) {
          console.log('1st call mounted');
          updateIndicatorState();
        }
      }, [mounted, updateIndicatorState]);
    

    只要mountedupdateIndicatorState 的值发生变化,就会触发此效果。

      const updateIndicatorState = useCallback(() => {
         ...
      }, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
    

    如果 updateIndicatorState 的 dep 数组中的任何值发生变化,updateIndicatorState 的值就会发生变化,即 getTabsMeta

    const getTabsMeta = useCallback(() => {
       ...
      }, [value, valueToIndex]);
    

    getTabsMeta 的值会随着valuevalueToIndex 的变化而变化。根据我从您的代码中收集到的信息,value 是所选选项卡的值,valueToIndex 是在该组件的每个渲染上重新定义的 Map。所以我希望getTabsMeta 的值也会在每个渲染上重新定义,这将导致包含updateIndicatorState 的useEffect 在每个渲染上运行。

    【讨论】:

    • 是的,但是为什么 updateIndicatorState 在 value prop 更改(选项卡更改)时触发两次?当我从 useEffect 的 deps 数组中删除 updateIndicatorState 并改为向 deps 添加值时 - 它仅在我更改选项卡时触发 updateIndicatorState 一次(应该是这样)。
    猜你喜欢
    • 1970-01-01
    • 2017-03-03
    • 2010-10-30
    • 2020-01-23
    • 2020-12-11
    • 1970-01-01
    • 2021-01-16
    • 2015-09-19
    • 1970-01-01
    相关资源
    最近更新 更多