渲染后对元素进行滚动反应



我正在使用React和Apollo Graphql创建一个应用程序。我的应用程序的一部分包括向用户显示选项列表,以便他可以选择一个。一旦他选择了其中一个,其他选项就会隐藏起来。

这是我的代码:

/**
* Renders a list of simple products.
*/
export default function SimplesList(props: Props) {
return (
<Box>
{props.childProducts
.filter(child => showProduct(props.parentProduct, child))
.map(child => (
<SingleSimple
key={child.id}
product={child}
menuItemCacheId={props.menuItemCacheId}
parentCacheId={props.parentProduct.id}
/>
))}
</Box>
);
}

实际元素:

export default function SingleSimple(props: Props) {
const classes = useStyles();
const [ref, setRef] = useState(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const refCallback = useCallback(node => {
setRef(node);
}, []);
const scrollToElement = useCallback(() => {
if (ref) {
ref.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={refCallback}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}

我的问题是:一旦用户从列表中选择了一个元素,我想将窗口滚动到该元素,因为他将有几个列表可供选择,在选择时可能会迷路。然而,我的组件正在使用这个流程:

1-用户单击给定的简单元素。

2-点击触发一个异步突变,选择这个元素而不是其他元素。

3-更新应用程序状态,并重新创建列表中的所有组件(未选择的组件将被过滤掉,显示已选择的组件(。

4-重新创建完成后,我想滚动到选定的组件。

问题是,当flipQuantity数量突变完成执行时,我调用scrollToElement回调,但它包含的ref是为未选择的元素,该元素不再在屏幕上呈现,因为新的元素将由SimplesList组件重新创建。

如何在最新的组件上启动scrollIntoView功能?

更新:

代码相同,但使用useRef挂钩:

export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}

更新2:

根据Kornflex的建议,我再次更改了我的组件,但它仍然不起作用:

export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [needScroll, setNeedScroll] = useState(false);
useEffect(() => {
if (needScroll) {
scrollToElement();
}
}, [ref]);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
setNeedScroll(true);
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}

现在我得到这个错误:

index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

我之前已经解决了这个问题,方法是在项目出现时向其添加一个本地状态标志:

apolloClient.mutate({
mutation: MY_MUTATE,
variables: { ... },
update: (proxy, { data: { result } }) => {
// We mark the item with the local prop `addedByThisSession` so that we know to
// scroll to it once mounted in the DOM.
apolloClient.cache.writeData({ id: `MyType:${result._id}`, data: { ... result, addedByThisSession: true } });
}
})

然后当它安装时,我强制滚动并清除标志:

import scrollIntoView from 'scroll-into-view-if-needed';
...
const GET_ITEM = gql`
query item($id: ID!) {
item(_id: $id) {
...
addedByThisSession @client
}
}
`;
...
const MyItem = (item) => {
const apolloClient = useApolloClient();
const itemEl = useRef(null);
useEffect(() => {
// Scroll this item into view if it's just been added in this session
// (i.e. not on another browser or tab)
if (item.addedByThisSession) {
scrollIntoView(itemEl.current, {
scrollMode: 'if-needed',
behavior: 'smooth',
});
// Clear the addedByThisSession flag
apolloClient.cache.writeFragment({
id: apolloClient.cache.config.dataIdFromObject(item),
fragment: gql`
fragment addedByThisSession on MyType {
addedByThisSession
}
`,
data: {
__typename: card.__typename,
addedByThisSession: false,
},
});
}
});
...

这样做意味着我可以将突变与项的呈现完全分离,并且我可以确信只有当项存在于DOM中时才会发生滚动。

最新更新