Как правильно ждать состояния для обновления / рендеринга вместо использования функции задержки / тайм-аута? - PullRequest
0 голосов
/ 21 марта 2019

Я постараюсь сохранить это вкратце, но я не уверен на 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()
    • Это вызывается в качестве аргумента в 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;

Ответы [ 3 ]

2 голосов
/ 21 марта 2019

Вы можете использовать хук useLayoutEffect, чтобы определить, были ли обновлены значения, и выполнить действие. Поскольку вы хотите определить, все ли значения были обновлены, вам нужно сравнить старые и новые значения в useEffect. Вы можете обратиться к приведенному ниже сообщению, чтобы узнать, как написать usePrevious custom hook

Как сравнить старые и новые значения в 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])
0 голосов
/ 21 марта 2019

Я думаю, что причина вашей задержки здесь необходима, так как вы рассчитываете на основе прямоугольников первого и последнего элемента, на которые влияет нажатие кнопки и выполняете анимацию прокрутки 500 мс. Таким образом, в результате ваши вычисления должны ждать анимации. Измените количество анимации и задержку вы увидите соотношение.

стиль, который я имел в виду.

@include transition(all 500ms ease);

Короче говоря, я думаю, что вы используете правильный путь, если у вас есть анимации, связанные с расчетом.

0 голосов
/ 21 марта 2019

setState принимает необязательный второй аргумент , который является обратным вызовом, который выполняется после обновления состояния и повторной визуализации компонента.

Другой вариант - componentDidUpdate метод жизненного цикла.

...