javascript - 如何正确等待状态更新/渲染而不是使用延迟/超时函数?

标签 javascript reactjs lodash react-hooks

我会尽量保持简短,但我不能 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()
    • 此函数查找事件导航项,检查它是否在视口(viewport)内,如果不在视口(viewport)内,则更改导航的左侧位置,使其成为屏幕上最左侧的导航项,并且通过 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;

最佳答案

您可以使用useLayoutEffect钩子(Hook)来确定值是否已更新并采取操作。由于要确定是否所有值都已更新,因此需要在 useEffect 中比较新旧值。您可以引用下面的文章来了解如何编写usePrevious自定义钩子(Hook)

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

关于javascript - 如何正确等待状态更新/渲染而不是使用延迟/超时函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55270125/

相关文章:

javascript - 使用 Lodash _.chain() 进行条件判断

javascript - 有没有办法在 Lodash 或 Underscore 中为 null 指定默认值?

javascript - 特定区域的 JQuery 模式弹出窗口

javascript - ng-options 在选择中不起作用

javascript - 尝试跨 iframe 选择元素

jquery - 使用哪个 URL 从 React 组件调用 symfony 中的 API 函数,所有这些都在同一个应用程序中?

javascript - 在 lodash 的属性上合并/加入 2 个数组对象

javascript - Bootstrap 4 导航选项卡更改页面而不是更改选项卡内容

javascript - React-Meteor "cannot read property X of undefined"来自另一个页面

javascript - 调用 API 后无法填充来自 Axios 的输入