渲染后滚动到元素

React scroll to element after render

我正在使用 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>
  );
}

我的问题是:一旦用户从列表中选择了一个元素,我想将 window 滚动到该元素,因为他将有多个列表可供选择,当他选择时可能会迷路选择他们。但是我的组件正在使用此流程:

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:

我按照 Kornflexx 的建议再次更改了我的组件,但它仍然无法正常工作:

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.

中时发生