Текст не прокручивается в правильном направлении в начале после смены направления.

Вопрос или проблема

У меня есть компонент, который должен отображать 4 разные секции текста. Я хочу, чтобы номер 1 был начальным текстом, а затем навигировать между ними так, чтобы при нажатии на номер 2 экран скроллился вверх и новый текст появлялся снизу, а если в противоположном направлении, то он будет скроллиться вниз (как длинный кусок бумаги). Сейчас это работает, но когда я меняю направление, он сначала скроллится в неправильном направлении перед тем, как скроллиться в правильном. Я понимаю, что что-то не так в моем компоненте, как он обновляется, но я не могу заставить это работать.

Основной компонент – WorkProcess.jsx

import React, { useState } from "react";
import { textOptions } from "../../../constants/textOptions";
import WorkProcessList from "./WorkProcessList";
import WorkProcessText from "./WorkProcessText";

function WorkProcess() {
  const [displayedText, setDisplayedText] = useState(textOptions[0].text);
  const [activeItem, setActiveItem] = useState(textOptions[0].key); // Установить activeItem в key

  const handleListItemClick = (text, key) => {
    setDisplayedText(text); // Обновить отображаемый текст
    setActiveItem(key); // Установить активный элемент в key
  };

  return (
    <section className="flex flex-col md:flex-row w-[90%] md:w-[80%] lg:w-[60%] justify-center z-10 font-primary">
      <div className="flex items-center justify-center w-full md:w-[20%]"></div>

      <div className="overflow-hidden w-full md:w-[600px] bg-bg h-[300px] rounded-[20px] shadow-inner flex items-center justify-center">
        <WorkProcessText displayedText={displayedText} activeKey={activeItem} />
      </div>

      <div className="text-center md:text-start bg-bg md:bg-none flex items-center justify-center md:justify-start w-full md:w-[20%] py-[3%] md:py-[0%]">
        <WorkProcessList
          activeItem={activeItem}
          handleListItemClick={handleListItemClick}
          textOptions={textOptions}
        />
      </div>
    </section>
  );
}

export default WorkProcess;

WorkProcessText.js

import React, { useRef, useLayoutEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

const WorkProcessText = ({ displayedText, activeKey }) => {
  const prevKeyRef = useRef(activeKey); // Отслеживает предыдущий activeKey
  const [direction, setDirection] = useState("up"); // Отслеживает направление скролла

  useLayoutEffect(() => {
    const prevKey = prevKeyRef.current;

    // Рассчитывает направление на основе сравнения текущего и предыдущего key
    if (activeKey > prevKey) {
      setDirection("up");
    } else if (activeKey < prevKey) {
      setDirection("down");
    }

    // Обновляет prevKeyRef текущим activeKey
    prevKeyRef.current = activeKey;
  }, [activeKey]); // Запускается синхронно после изменений activeKey

  return (
    <AnimatePresence mode="wait">
      <motion.p
        key={displayedText} // Запускает анимацию при изменении текста
        className="text-s text-center w-[90%]"
        initial={{ y: direction === "up" ? 300 : -300 }} // Начинает ниже для 'up', выше для 'down'
        animate={{ y: 0 }} // Анимирует в центр
        exit={{ y: direction === "up" ? -300 : 300 }} // Выходит выше для 'up', ниже для 'down'
        transition={{ duration: 0.3, ease: "easeInOut" }} // Плавный переход
      >
        {displayedText}
      </motion.p>
    </AnimatePresence>
  );
};

export default WorkProcessText;

WorkProcessList.js

import React from "react";

const WorkProcessList = ({ activeItem, handleListItemClick, textOptions }) => {
  return (
    <ul className="text-l leading-none ml-5">
      {textOptions.map(({ key, title, text }) => (
        <li
          key={key}
          onClick={() => handleListItemClick(text, key)} // Вызывает key вместо item
          className={`transition duration-300 ease-in-out transform hover:text-blue-500 cursor-pointer ${
            activeItem === key ? "text-blue-500" : "" // Сравнивает activeItem с key
          }`}
        >
          {title}
        </li>
      ))}
    </ul>
  );
};

export default WorkProcessList;

textOptions.js

export const textOptions = [
  {
    key: 1,
    title: "ONE",
    text: "text one",
  },
  {
    key: 2,
    title: "TWO",

    text: "text two",
  },
  {
    key: 3,
    title: "THREE",
    text: "text three",
  },
  {
    key: 4,
    title: "FOUR",
    text: "text four",
  },
];

В вашем useLayoutEffect вы сравниваете состояние activeKey с prevKeyRef.current сразу после установки activeKey. Однако, поскольку setState асинхронен в React, значение activeKey, используемое при вычислении направления, может быть предыдущим, когда оно оценивается.

Замените useLayoutEffect на useEffect, чтобы правильно отслеживать предыдущее значение activeKey и обновлять направление скролла после рендеринга компонента.

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>


import React, { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";

const WorkProcessText = ({ displayedText, activeKey }) => {
  const prevKeyRef = useRef(activeKey); // Отслеживает предыдущий activeKey
  const [direction, setDirection] = useState("up"); // Отслеживает направление скролла

  // Этот эффект будет отслеживать изменения activeKey и обновлять направление
  useEffect(() => {
    const prevKey = prevKeyRef.current;

    // Рассчитывает направление на основе сравнения текущего и предыдущего key
    if (activeKey > prevKey) {
      setDirection("up");
    } else if (activeKey < prevKey) {
      setDirection("down");
    }

    // Обновляет prevKeyRef текущим activeKey после установки направления
    prevKeyRef.current = activeKey;
  }, [activeKey]); // Это будет выполняться каждый раз, когда activeKey изменяется

  return (
    <AnimatePresence mode="wait">
      <motion.p
        key={displayedText} // Запускает анимацию при изменении текста
        className="text-s text-center w-[90%]"
        initial={{ y: direction === "up" ? 300 : -300 }} // Начинает ниже для 'up', выше для 'down'
        animate={{ y: 0 }} // Анимирует в центр
        exit={{ y: direction === "up" ? -300 : 300 }} // Выходит выше для 'up', ниже для 'down'
        transition={{ duration: 0.3, ease: "easeInOut" }} // Плавный переход
      >
        {displayedText}
      </motion.p>
    </AnimatePresence>
  );
};

export default WorkProcessText;

.

Ответ или решение

Вопрос: Почему текст не прокручивается в правильном направлении?

При разработке интерфейса, в котором текст должен плавно прокручиваться вниз или вверх в зависимости от выбранной секции, может возникнуть проблема с неправильным направлением прокрутки в начале. Данная ситуация связана с тем, что обновление состояния в React выполняется асинхронно, и это может приводить к разночтениям между предыдущими и текущими значениями состояния.

Основная проблема

В вашем коде используется хук useLayoutEffect, чтобы отслеживать изменения активного элемента (activeKey) и вычислять направление прокрутки. Однако, как вы правильно отметили, из-за асинхронного поведения функции setState, значение activeKey может не обновляться вовремя, что приводит к некорректному вычислению направления прокрутки на первом кадре после изменения.

Решение

Рекомендуется заменить хук useLayoutEffect на useEffect, что позволит обновить направление прокрутки только после рендеринга компонента. Это обеспечит использование актуального значения activeKey для вычисления направления.

Вот обновлённый код вашего компонента WorkProcessText, который реализует данное решение:

import React, { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";

const WorkProcessText = ({ displayedText, activeKey }) => {
  const prevKeyRef = useRef(activeKey); // Хранит предыдущий активный ключ
  const [direction, setDirection] = useState("up"); // Отслеживает направление прокрутки

  useEffect(() => {
    const prevKey = prevKeyRef.current;

    // Вычисляем направление прокрутки на основе текущего и предыдущего ключа
    if (activeKey > prevKey) {
      setDirection("up");
    } else if (activeKey < prevKey) {
      setDirection("down");
    }

    // Обновляем предыдущее значение activeKey
    prevKeyRef.current = activeKey;
  }, [activeKey]); // Этот эффект выполняется при каждом изменении activeKey

  return (
    <AnimatePresence mode="wait">
      <motion.p
        key={displayedText} // Запускаем анимацию при изменении текста
        className="text-s text-center w-[90%]"
        initial={{ y: direction === "up" ? 300 : -300 }} // Начальный пункт для 'up' ниже, для 'down' выше
        animate={{ y: 0 }} // Анимация в центр
        exit={{ y: direction === "up" ? -300 : 300 }} // Выход выше для 'up', ниже для 'down'
        transition={{ duration: 0.3, ease: "easeInOut" }} // Плавный переход
      >
        {displayedText}
      </motion.p>
    </AnimatePresence>
  );
};

export default WorkProcessText;

Заключение

Изменив useLayoutEffect на useEffect, вы обеспечите корректную работу своего компонента, анимация текста будет производиться в соответствии с ожидаемым поведением без ошибок при переключении между направлениями прокрутки.

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

Оцените материал
Добавить комментарий

Капча загружается...