【问题标题】:How to correctly wait on state to update/render instead of using a delay/timeout function?如何正确等待状态更新/渲染而不是使用延迟/超时功能?
【发布时间】:2019-03-20 21:01:21
【问题描述】:

我会尽量保持简短,但我不能 100% 确定实现目标的正确方法。在没有太多培训的情况下,我已经被 React 深陷其中,所以我很可能错误地处理了这个组件的大部分内容,正确的方向肯定会有所帮助,我真的不希望有人完全重做我的组件对我来说很长。

我有一个导航栏SubNav,它根据url/路径找到当前活动的项目,然后这将移动一个继承活动元素宽度的下划线元素。为此,我找到了活动项目的位置和相应的位置。当用户将鼠标悬停在另一个导航项上时,或者当窗口调整大小时它会相应地调整位置时也是如此。

当分辨率较低时,当导航被切断以使箭头在导航上向左/向右滚动以查看所有导航项时,我也有它。

此外,如果在较低分辨率下并且当前活动的导航项不在屏幕上,导航将滚动到该项目,然后正确定位下划线。

这个,目前可以在我的组件中使用,这个问题是,我不相信我做对了,我正在使用 lodash 函数 delay 来延迟某些点(我猜获取某些导航项的正确位置,因为它在函数调用时不正确),我觉得这不是要走的路。这完全取决于页面加载的速度等,并且对于每个用户来说都是不同的。

_.delay(
        () => {
          setSizes(getSizes()),
            updateRightArrow(findItemInView(elsRef.length - 1)),
            updateLeftArrow(findItemInView(0));
        },
        400,
        setArrowStyle(styling)
      );

不使用延迟,从我的状态返回的值是不正确的,因为它们尚未设置。

我的问题是,我该如何正确处理这个问题?我知道我下面的代码有点可读,但我提供了一个 CODESANBOX 来玩。

我有 3 个主要功能,它们相互依赖:

  1. getPostion()
    • 此函数查找活动导航项,检查它是否在视口内,如果不在,则更改导航的left 位置,使其成为屏幕上最左侧的导航项,并通过setSizes(getSizes()) 移动正下方的下划线。
  2. getSizes()
    • 这在setSizes 中作为参数调用以更新sizes 状态,该状态返回所有导航项的左右边界
  3. getUnderlineStyle()
    • 这在getSizes() 函数中作为setUnderLineStyle 中的参数调用,以更新下划线对象相对于从sizes 状态抓取的活动导航项的位置的位置,但我必须通过sizesObj 作为setSizes 中的参数,因为状态尚未设置。我想这就是我的困惑开始的地方,我想我的印象是,当我设置状态时,我可以访问它。于是,我开始用delay来实战。

下面是我的整个组件,但可以看到在 CODESANBOX 中工作

import React, { useEffect, useState, useRef } from "react";
import _ from "lodash";
import { Link, Route } from "react-router-dom";
import "../../scss/partials/_subnav.scss";

const SubNav = props => {
  const subNavLinks = [
    {
      section: "Link One",
      path: "link1"
    },
    {
      section: "Link Two",
      path: "link2"
    },
    {
      section: "Link Three",
      path: "link3"
    },
    {
      section: "Link Four",
      path: "link4"
    },
    {
      section: "Link Five",
      path: "link5"
    },
    {
      section: "Link Six",
      path: "link6"
    },
    {
      section: "Link Seven",
      path: "link7"
    },
    {
      section: "Link Eight",
      path: "link8"
    }
  ];

  const currentPath =
    props.location.pathname === "/"
      ? "link1"
      : props.location.pathname.replace(/\//g, "");

  const [useArrows, setUseArrows] = useState(false);
  const [rightArrow, updateRightArrow] = useState(false);
  const [leftArrow, updateLeftArrow] = useState(false);

  const [sizes, setSizes] = useState({});

  const [underLineStyle, setUnderLineStyle] = useState({});
  const [arrowStyle, setArrowStyle] = useState({});

  const [activePath, setActivePath] = useState(currentPath);

  const subNavRef = useRef("");
  const subNavListRef = useRef("");
  const arrowRightRef = useRef("");
  const arrowLeftRef = useRef("");
  let elsRef = Array.from({ length: subNavLinks.length }, () => useRef(null));

  useEffect(
    () => {
      const reposition = getPosition();
      subNavArrows(window.innerWidth);
      if (!reposition) {
        setSizes(getSizes());
      }
      window.addEventListener(
        "resize",
        _.debounce(() => subNavArrows(window.innerWidth))
      );
      window.addEventListener("resize", () => setSizes(getSizes()));
    },
    [props]
  );

  const getPosition = () => {
    const activeItem = findActiveItem();
    const itemHidden = findItemInView(activeItem);
    if (itemHidden) {
      const activeItemBounds = elsRef[
        activeItem
      ].current.getBoundingClientRect();
      const currentPos = subNavListRef.current.getBoundingClientRect().left;
      const arrowWidth =
        arrowLeftRef.current !== "" && arrowLeftRef.current !== null
          ? arrowLeftRef.current.getBoundingClientRect().width
          : arrowRightRef.current !== "" && arrowRightRef.current !== null
          ? arrowRightRef.current.getBoundingClientRect().width
          : 30;

      const activeItemPos =
        activeItemBounds.left * -1 + arrowWidth + currentPos;

      const styling = {
        left: `${activeItemPos}px`
      };

      _.delay(
        () => {
          setSizes(getSizes()),
            updateRightArrow(findItemInView(elsRef.length - 1)),
            updateLeftArrow(findItemInView(0));
        },
        400,
        setArrowStyle(styling)
      );

      return true;
    }

    return false;
  };

  const findActiveItem = () => {
    let activeItem;
    subNavLinks.map((i, index) => {
      const pathname = i.path;
      if (pathname === currentPath) {
        activeItem = index;
        return true;
      }
      return false;
    });

    return activeItem;
  };

  const getSizes = () => {
    const rootBounds = subNavRef.current.getBoundingClientRect();

    const sizesObj = {};

    Object.keys(elsRef).forEach(key => {
      const item = subNavLinks[key].path;
      const el = elsRef[key];
      const bounds = el.current.getBoundingClientRect();

      const left = bounds.left - rootBounds.left;
      const right = rootBounds.right - bounds.right;

      sizesObj[item] = { left, right };
    });

    setUnderLineStyle(getUnderlineStyle(sizesObj));

    return sizesObj;
  };

  const getUnderlineStyle = (sizesObj, active) => {
    sizesObj = sizesObj.length === 0 ? sizes : sizesObj;
    active = active ? active : currentPath;

    if (active == null || Object.keys(sizesObj).length === 0) {
      return { left: "0", right: "100%" };
    }

    const size = sizesObj[active];

    const styling = {
      left: `${size.left}px`,
      right: `${size.right}px`,
      transition: `left 300ms, right 300ms`
    };

    return styling;
  };

  const subNavArrows = windowWidth => {
    let totalSize = sizeOfList();

    _.delay(
      () => {
        updateRightArrow(findItemInView(elsRef.length - 1)),
          updateLeftArrow(findItemInView(0));
      },
      300,
      setUseArrows(totalSize > windowWidth)
    );
  };

  const sizeOfList = () => {
    let totalSize = 0;

    Object.keys(elsRef).forEach(key => {
      const el = elsRef[key];
      const bounds = el.current.getBoundingClientRect();

      const width = bounds.width;

      totalSize = totalSize + width;
    });

    return totalSize;
  };

  const onHover = active => {
    setUnderLineStyle(getUnderlineStyle(sizes, active));
    setActivePath(active);
  };

  const onHoverEnd = () => {
    setUnderLineStyle(getUnderlineStyle(sizes, currentPath));
    setActivePath(currentPath);
  };

  const scrollRight = () => {
    const currentPos = subNavListRef.current.getBoundingClientRect().left;
    const arrowWidth = arrowRightRef.current.getBoundingClientRect().width;
    const subNavOffsetWidth = subNavRef.current.clientWidth;

    let nextElPos;
    for (let i = 0; i < elsRef.length; i++) {
      const bounds = elsRef[i].current.getBoundingClientRect();
      if (bounds.right > subNavOffsetWidth) {
        nextElPos = bounds.left * -1 + arrowWidth + currentPos;
        break;
      }
    }

    const styling = {
      left: `${nextElPos}px`
    };

    _.delay(
      () => {
        setSizes(getSizes()),
          updateRightArrow(findItemInView(elsRef.length - 1)),
          updateLeftArrow(findItemInView(0));
      },
      500,
      setArrowStyle(styling)
    );
  };

  const scrollLeft = () => {
    const windowWidth = window.innerWidth;
    // const lastItemInView = findLastItemInView();
    const firstItemInView = findFirstItemInView();
    let totalWidth = 0;
    const hiddenEls = elsRef
      .slice(0)
      .reverse()
      .filter((el, index) => {
        const actualPos = elsRef.length - 1 - index;
        if (actualPos >= firstItemInView) return false;
        const elWidth = el.current.getBoundingClientRect().width;
        const combinedWidth = elWidth + totalWidth;
        if (combinedWidth > windowWidth) return false;
        totalWidth = combinedWidth;
        return true;
      });

    const targetEl = hiddenEls[hiddenEls.length - 1];

    const currentPos = subNavListRef.current.getBoundingClientRect().left;
    const arrowWidth = arrowLeftRef.current.getBoundingClientRect().width;
    const isFirstEl =
      targetEl.current.getBoundingClientRect().left * -1 + currentPos === 0;

    const targetElPos = isFirstEl
      ? targetEl.current.getBoundingClientRect().left * -1 + currentPos
      : targetEl.current.getBoundingClientRect().left * -1 +
        arrowWidth +
        currentPos;

    const styling = {
      left: `${targetElPos}px`
    };

    _.delay(
      () => {
        setSizes(getSizes()),
          updateRightArrow(findItemInView(elsRef.length - 1)),
          updateLeftArrow(findItemInView(0));
      },
      500,
      setArrowStyle(styling)
    );
  };

  const findItemInView = pos => {
    const rect = elsRef[pos].current.getBoundingClientRect();

    return !(
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= window.innerHeight &&
      rect.right <= window.innerWidth
    );
  };

  const findLastItemInView = () => {
    let lastItem;
    for (let i = 0; i < elsRef.length; i++) {
      const isInView = !findItemInView(i);
      if (isInView) {
        lastItem = i;
      }
    }
    return lastItem;
  };

  const findFirstItemInView = () => {
    let firstItemInView;
    for (let i = 0; i < elsRef.length; i++) {
      const isInView = !findItemInView(i);
      if (isInView) {
        firstItemInView = i;
        break;
      }
    }
    return firstItemInView;
  };

  return (
    <div
      className={"SubNav" + (useArrows ? " SubNav--scroll" : "")}
      ref={subNavRef}
    >
      <div className="SubNav-content">
        <div className="SubNav-menu">
          <nav className="SubNav-nav" role="navigation">
            <ul ref={subNavListRef} style={arrowStyle}>
              {subNavLinks.map((el, i) => (
                <Route
                  key={i}
                  path="/:section?"
                  render={() => (
                    <li
                      ref={elsRef[i]}
                      onMouseEnter={() => onHover(el.path)}
                      onMouseLeave={() => onHoverEnd()}
                    >
                      <Link
                        className={
                          activePath === el.path
                            ? "SubNav-item SubNav-itemActive"
                            : "SubNav-item"
                        }
                        to={"/" + el.path}
                      >
                        {el.section}
                      </Link>
                    </li>
                  )}
                />
              ))}
            </ul>
          </nav>
        </div>
        <div
          key={"SubNav-underline"}
          className="SubNav-underline"
          style={underLineStyle}
        />
      </div>
      {leftArrow ? (
        <div
          className="SubNav-arrowLeft"
          ref={arrowLeftRef}
          onClick={scrollLeft}
        />
      ) : null}
      {rightArrow ? (
        <div
          className="SubNav-arrowRight"
          ref={arrowRightRef}
          onClick={scrollRight}
        />
      ) : null}
    </div>
  );
};

export default SubNav;

【问题讨论】:

    标签: javascript reactjs lodash react-hooks


    【解决方案1】:

    您可以使用useLayoutEffect 挂钩来确定值是否已更新并采取措施。由于要确定是否所有值都已更新,因此需要比较 useEffect 中的新旧值。您可以参考下面的帖子了解如何编写usePrevious 自定义钩子

    How to compare oldValues and newValues on React Hooks useEffect?

    const oldData = usePrevious({ rightArrow, leftArrow, sizes});
    useLayoutEffect(() => {
       const {rightArrow: oldRightArrow, leftArrow: oldLeftArrow, sizes: oldSizes } = oldData;
      if(oldRightArrow !== rightArrow && oldLeftArrow !== leftArrow and oldSizes !== sizes) {
          setArrowStyle(styling)
      }
    }, [rightArrow, leftArrow, sizes])
    

    【讨论】:

      【解决方案2】:

      我认为您延迟的原因在这里是必要的,因为您根据单击按钮并执行滚动 500 毫秒动画时受影响的第一个和最后一个元素的矩形进行计算。因此,您的计算需要等待动画完成。改变动画和延迟的数量你会看到关系。

      我的意思。

      @include transition(all 500ms ease);
      

      简而言之,只要你有与计算相关的动画,我认为你使用的是正确的方法。

      【讨论】:

      • 谢谢,这实际上很有意义。将延迟更新到500 可能是最好的方法。我可能会尝试删除该转换,看看当我再次使用我的机器时是否不需要延迟。
      【解决方案3】:

      setState 采用可选的second argument,它是在状态更新和组件重新渲染后执行的回调。

      另一个选项是componentDidUpdate 生命周期方法。

      【讨论】:

      • 感谢您的反馈,但我使用的是基于函数的组件,而不是基于类的组件,因此无法使用setStatecomponentDidUpdate 生命周期方法
      猜你喜欢
      • 2019-02-21
      • 2023-01-14
      • 1970-01-01
      • 1970-01-01
      • 2020-07-06
      • 2021-01-17
      • 2023-03-20
      • 1970-01-01
      • 2021-10-15
      相关资源
      最近更新 更多