Вопрос или проблема
У меня есть компонент, который должен отображать 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 и управления состоянием являются ключевыми компонентами для создания эффективных и интуитивно понятных интерфейсов. Данная практика не только решает текущую проблему, но и способствует улучшению следования хорошим стандартам разработки в будущем.