渲染后将滚动滚动到元素

时间:2019-11-18 13:31:33

标签: javascript reactjs apollo react-apollo

我正在使用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回调,但是其中包含的引用是针对未选定元素的,不再在屏幕上呈现,因为新组件将由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.

1 个答案:

答案 0 :(得分:0)

我以前通过在要滚动显示的项目上添加本地状态标志来解决此问题:

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中时才进行滚动。