繁体   English   中英

如何正确等待状态更新/渲染,而不使用延迟/超时功能?

[英]How to correctly wait on state to update/render instead of using a delay/timeout function?

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

我有一个导航栏SubNav ,它基于url / path查找当前处于活动状态的项目,然后将移动下划线元素,该元素继承了活动元素的宽度。 为此,我找到了活动项目的位置以及相应的位置。 当用户将鼠标悬停在另一个导航项上或调整窗口大小时,它也会相应地调整位置。

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

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

这目前可以在我的组件中使用,但这个问题是,我不相信我已经正确地做到了,我正在使用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()
    • 这被称作中的参数setUnderLineStyle的内getSizes()函数来更新下划线对象的相对于活动导航项目的位置从所抓取的位置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;

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

如何在React Hooks useEffect上比较oldValues和newValues?

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])

setState使用可选的第二个参数 ,该参数是在状态已更新并且组件已重新呈现之后执行的回调。

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

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

我的意思是风格

@include transition(all 500ms ease);

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM