我的 React 组件有这个问题,我无法弄清楚。 每次重新渲染时它都会滚动回顶部。 而且,我一开始就不明白为什么它要重新渲染。 基本上我有一个祖父母、父母和孙子组件。 孙子有一个单击事件,该事件显示祖父母的不同孩子(如果有任何意义的话)。 该单击事件会执行一些操作,但它会导致祖组件重新渲染,从而导致父组件滚动到顶部。 我将添加我的代码,希望能够澄清我在这里所说的内容。 只是想提供一些背景信息。
所以主要问题是滚动到顶部。 如果我们能找出它重新渲染的原因并停止它,那就是一个奖励。
祖父母(StaticLine.js):
import React, { useEffect, useState } from 'react';
import StaticOrderColumn from '../ordercolumn/StaticOrderColumn';
import { useGlobalLineTypeActionsContext } from '../../context/GlobalLineTypeContext';
import Details from '../details/DetailsColumn';
import { ModalStyles } from '../modals/ModalStyles';
import {
useScheduleActionsContext,
useScheduleContext
} from '../../context/ScheduleContext';
import PalletCount from './PalletCount';
import OrderButtons from './OrderButtons';
import PropTypes from 'prop-types';
import {
useGlobalShowDetailsActionsContext,
useGlobalShowDetailsContext
} from '../../context/GlobalShowDetailsContext';
import { useTraceUpdate } from '../../utils/hooks';
const StaticLine = (props) => {
useTraceUpdate(props);
console.log('top of StaticLine');
const { type, setTitle } = props;
const { orderType, orders } = useScheduleContext();
const { setOrders, setOrderType } = useScheduleActionsContext();
const setGlobalLineType = useGlobalLineTypeActionsContext();
const showDetails = useGlobalShowDetailsContext();
const setShowDetails = useGlobalShowDetailsActionsContext();
const [lineID, setLineID] = useState(-1);
const [lineTitle, setLineTitle] = useState('');
const [highlightDest, setHighlightDest] = useState(false);
const ordersCol = React.useRef();
let lineCountOffset = 0;
let numLines = 4;
let pageTitle = '';
switch (type) {
case 'bagline':
pageTitle += 'Bag Lines';
break;
case 'toteline':
pageTitle += 'Tote Lines';
lineCountOffset = 10;
break;
case 'otherline':
pageTitle += 'Other Lines';
numLines = 3;
lineCountOffset = 4;
break;
default:
}
useEffect(() => {
setLineID(-1);
}, [type]);
const globalLineType = type + 's';
useEffect(() => {
setGlobalLineType(globalLineType);
setTitle(pageTitle);
const title = `${process.env.REACT_APP_BASE_TITLE ||
'title'} - ${pageTitle}`;
document.title = title;
}, [type, setGlobalLineType, pageTitle, setTitle, globalLineType]);
const selectLine = (e) => {
setShowDetails(false);
setHighlightDest(false);
const lineNum = e.target.value.substring(4);
setLineID(parseInt(lineNum, 10));
setLineTitle(
orderType.charAt(0).toUpperCase() +
orderType.slice(1) +
' Orders - Line ' +
parseInt(lineNum, 10)
);
};
const selectOrderType = (e) => {
const selectedOrderType = e.target.value;
setOrderType(selectedOrderType);
setShowDetails(false);
setLineTitle(
selectedOrderType.charAt(0).toUpperCase() +
selectedOrderType.slice(1) +
' Orders - Line ' +
lineID
);
};
const OrderColWithRef = React.forwardRef((props, ref) => (
<StaticOrderColumn
{...props}
title={lineTitle}
lineID={lineID}
orders={orders}
ref={ref}
/>
));
return (
<div
className={`staticLines p-1 no-gutters d-flex flex-nowrap${
orderType === 'completed' ? ' completed' : ''
}`}
>
<div className={'radio-col no-border'}>
<div className={'radio-container p-2'}>
<div className={'radios'}>
// lots of irrelevant code here
</div>
</div>
</div>
{lineID > -1 && (
<>
<div
className={'col lines no-gutters order-col'}
>
<OrderColWithRef ref={ordersCol} />
</div>
<div className={'col row lines no-gutters order-details'}>
<Details
setOrders={setOrders}
orders={orders || []}
customStyles={ModalStyles}
highlightDest={highlightDest}
setHighlightDest={setHighlightDest}
errLocation={'top-center'}
/>
{orderType === 'completed' && showDetails && (
<OrderButtons
setLineID={setLineID}
setOrders={setOrders}
orders
lineNum={lineID}
/>
)}
</div>
<div className={'col lines no-gutters d-flex no-border'}>
{orderType === 'scheduled' && (
<PalletCount
type={'Bag'}
lineNum={lineID}
orders={orders}
setTitle={setTitle}
setHighlightDest={setHighlightDest}
/>
)}
</div>
</>
)}
</div>
);
};
StaticLine.propTypes = {
type: PropTypes.string.isRequired,
orders: PropTypes.array,
setTitle: PropTypes.func.isRequired
};
export default StaticLine;
父级(StaticOrderColumn.js):
import React from 'react';
import PropTypes from 'prop-types';
import StaticOrder from '../order/StaticOrder';
import '../../scss/App.scss';
import { useGlobalSpinnerContext } from '../../context/GlobalSpinnerContext';
const StaticOrderColumn = (props) => {
const { title, lineID, orders } = props;
const isGlobalSpinnerOn = useGlobalSpinnerContext();
const sortedOrdersIDs = orders
.filter((o) => o.lineNum === lineID)
.sort((a, b) => a.linePosition - b.linePosition)
.map((o) => o.id);
return (
<div id={'line-0'} className={'col order-column'}>
<header className={'text-center title'}>
{title}{' '}
{sortedOrdersIDs.length > 0 && (
<span> ({sortedOrdersIDs.length})</span>
)}
</header>
<div className={'orders'}>
{orders &&
sortedOrdersIDs &&
sortedOrdersIDs.map((orderID, index) => {
const order = orders.find((o) => o.id === orderID);
return (
<StaticOrder
key={orderID}
order={order}
index={index}
/>
);
})}
{!sortedOrdersIDs.length && !isGlobalSpinnerOn && (
<h3>There are no orders on this line.</h3>
)}
</div>
</div>
);
};
StaticOrderColumn.propTypes = {
title: PropTypes.string.isRequired,
lineID: PropTypes.number.isRequired,
orders: PropTypes.array.isRequired,
ref: PropTypes.instanceOf(Element).isRequired
};
export default StaticOrderColumn;
此文件是单击事件发生的位置,并导致 StaticLine 重新渲染并滚动到 StaticOrderColumn 的顶部。 孙子(StaticOrder.js):
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import '../../scss/App.scss';
import { getFormattedDate } from '../../utils/utils';
import { useGlobalActiveOrderActionsContext } from '../../context/GlobalActiveOrderContext';
import { useGlobalShowDetailsActionsContext } from '../../context/GlobalShowDetailsContext';
// import { stringTrunc } from '../../utils/utils';
const MyOrder = styled.div`
background-color: #193df4;
transition: background-color 1s ease;
`;
const devMode = true;
// const devMode = false;
const StaticOrder = (props) => {
const {
id,
item,
desc,
cust,
palletsOrd,
bagID,
chemicals,
totalBagsUsed,
linePosition,
palletsRem,
palletCount,
requestDate,
orderNumber,
comments
} = props.order;
const setActiveOrder = useGlobalActiveOrderActionsContext();
const setShowDetails = useGlobalShowDetailsActionsContext();
const orderID = id + '';
// show the details section when user clicks an order
// THIS IS WHERE THE ISSUE IS HAPPENING, WHEN THE ORDER IS CLICKED,
// THIS FUNCTION RUNS AND THE StaticLine COMPONENT RE-RENDERS AND THE StaticOrderColumn SCROLLS TO THE TOP
const showDetails = (orderID) => {
setActiveOrder(parseInt(orderID, 10));
setShowDetails(true);
};
return (
<MyOrder
id={orderNumber}
className={'order static'}
onClick={(e) => showDetails(orderID, e)}
>
{/*<div className={'orderID'}>{id}</div>*/}
<p className={'item-number'}>
{item !== '' ? `Item Number: ${item}` : ''}
</p>
<p>{desc !== '' ? `NPK: ${desc}` : ''}</p>
<p>{cust !== '' ? `Customer: ${cust}` : ''}</p>
<p>
{palletsOrd !== '' ? `Pallets Ordered: ${palletsOrd}` : ''}
</p>
<p>{bagID !== '' ? `Bag ID: ${bagID}` : ''}</p>
<p>{chemicals !== '' ? `Chemical : ${chemicals}` : ''}</p>
<p>
{requestDate !== ''
? `Request Date: ${getFormattedDate(new Date(requestDate))}`
: ''}
</p>
{devMode && (
<>
<div className={'id-line-num-pos'}>
<p>OrderID: {orderNumber}</p>
</div>
</>
)}
<div className={'total-bags'}>Total Bags: {totalBagsUsed}</div>
<div className={'pallets-remaining'}>
Pallets Left: {palletsRem}
</div>
<div className={'pallets-done'}>
Pallets Done: {palletCount}
</div>
<div className={'line-position'}>{linePosition + 1}</div>
{comments.length > 0 && (
// bunch of SVG code
)}
</MyOrder>
);
};
StaticOrder.propTypes = {
order: PropTypes.object,
id: PropTypes.number,
index: PropTypes.number,
title: PropTypes.string,
orderID: PropTypes.string
};
export default StaticOrder;
编辑:我添加了问题的图片,以帮助大家直观地了解它。 订单框位于该图像的左侧。 默认情况下,“订单详细信息”是隐藏的。 单击左侧的订单时,它会将该订单加载到“订单详细信息”中并显示该组件。 发生这种情况时,左侧的订单列会滚动回顶部。 在随后的订单点击中,它不会滚动到顶部。 仅当显示或隐藏“订单详细信息”时,它才会滚动回顶部。
编辑2:我发现如果我从StaticLine.js中取出
const showDetails = useGlobalShowDetailsContext();
行和对showDetails
的2个引用,这个问题就会消失。 因此,如果这可以帮助任何人解决问题......
编辑3:我慢慢地向前走。 我想出了如何删除
showDetails
文件中对 StaticLine.js
的引用之一。 现在,如果有人可以帮助我弄清楚如何从该组件中获取最后一个引用,但保留功能,那就太棒了!
提醒一下,这是我正在谈论的参考:
{orderType === 'completed' && showDetails && (
<OrderButtons
setLineID={setLineID}
setOrders={setOrders}
orders
lineNum={lineID}
/>
)}
如果需要更多信息或更多代码,请告诉我。 任何见解将非常感激。
const OrderColWithRef = React.forwardRef((props, ref) => (
<StaticOrderColumn
{...props}
title={lineTitle}
lineID={lineID}
orders={orders}
ref={ref}
/>
));
将其作为顶级函数移到
StaticLine
之外。
React 足够智能,可以在仅部分 Dom 更改时避免重新创建 html 元素和安装内容。如果只有一个 prop 发生变化,它将保留该元素并仅更改其值等。这是通过比较
element.type
来完成的。
您实际上正在做的是在每个渲染上创建一个新的
OrderColWithRef
函数,因为它是本地函数,所以类型不相等。每次 StaticLine
中的任何内容发生变化时,React 都会卸载并重新挂载一个新的 html 元素。
永远不要嵌套组件声明。在函数内声明组件有效的唯一情况是 HOC,即使如此,HOC 函数本身也不是有效元素,只有它的返回值才是。
希望这能解决问题。
我的问题是将接收新数据的事物视为组件,而不是返回组件的函数。
将
<NewsList />
更改为 {newsList()}
就成功了。
您尝试过使用
getSnapshotBeforeUpdate()
吗?根据官方文档:
在最近渲染的输出提交给例如之前调用DOM。它使您的组件能够在 DOM 发生潜在更改之前捕获一些信息(例如滚动位置)。
基本上,这个想法是您可以在重新渲染发生之前访问当前滚动位置,从
getSnapshotBeforeUpdate()
返回
在组件更新之后,使用 componentDidUpdate() 您可以访问从 getSnapshotBeforeUpdate()
返回的值,并将滚动位置设置为重新渲染之前的位置。借用官方文档的例子:
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
我强烈建议使用 @welldone-software/why-did-you-render 包来让您深入了解组件渲染的原因。
参考@Mordechai 接受的答案。 我取出了正在重新渲染的组件,但我意识到将道具传递到其中似乎工作量太大。 我所做的只是拉出滚动视图。
{
...
return (
<>
<ScrollView>
<ReviewLayout />
</ScrollView>
</>
);
}
const ScrollView = ({children}: any)=>{
return(
<Box style={{width: '100%', height: 500, overflowY: 'scroll'}}>
{children}
</Box>
);
}
React v18.2.0(Typescript)也有同样的问题。我在这个平台或博客上找到的解决方案都不起作用。 这是我的解决方案,不使用任何第三方,
ref
或setTimeout
。花费几个小时来解决,因此分享以帮助人们。
就我而言,我需要每秒获取数据并相应地更新 DOM。由于状态的异步行为,我使用局部变量来存储滚动位置,而不是反应状态。下面我简化了我的代码,删除了与反应渲染滚动问题无关的业务逻辑。
import React, { useEffect, useState } from 'react';
const SignalDetails = () => {
// code block for "set scroll to the user's position"
let posY = 0;
let prevPosY = 0;
let reloading = false;
const onScroll: EventListener = (event: Event) => {
posY = window.scrollY; // get the scroll position on-scroll
};
useEffect(() => {
if (!reloading){ // do not change event listener during the data fetch
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}
}, []);
useEffect(() => {
const fetchData = async () => {
prevPosY = posY;
reloading = true; // set data loading flag true
await getData(id).then(res => {
if (res.Success) {
// after some data operations
// send the scroll to the original position
window.scrollTo({
top: prevPosY,
left: 0,
behavior: 'instant',
})
}
reloading = false; // set data loading flag false
});
};
const intrv = setInterval(() => fetchData(), 1000);
return () => clearInterval(intrv);
}, [id]);
// some more code here...
return (
<>
<div>some HTML here</div>
</>
);
};
export default SignalDetails;