简体   繁体   English

渲染后将滚动反应到元素

[英]React scroll to element after render

I am creating an app using React and Apollo Graphql.我正在使用 React 和 Apollo Graphql 创建一个应用程序。 Part of my app consist of showing a list of options to the user so he can pick one.我的应用程序的一部分包括向用户显示选项列表,以便他可以选择一个。 Once he picks one of them, the other options are hidden.一旦他选择了其中一个,其他选项就被隐藏了。

Here is my code:这是我的代码:

/**
 * 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>
  );
}

And the actual element:和实际的元素:

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>
  );
}

My problem is this: once the user selects an element from the list, I would like to scroll the window to that element, because he will have several lists to choose from and he can get lost when choosing them.我的问题是:一旦用户从列表中选择了一个元素,我想将 window 滚动到该元素,因为他将有几个列表可供选择,并且在选择它们时可能会迷路。 However my components are using this flow:但是我的组件正在使用这个流程:

1- The user clicks on a given simple element. 1- 用户点击给定的简单元素。

2- This click fires an async mutation that chooses this element over the others. 2-此单击触发异步突变,该突变选择此元素而不是其他元素。

3- The application state is updated and all components from the list are re-created (the ones that were not selected are filtered out and the one that was selected is displayed). 3- 应用程序 state 已更新并重新创建列表中的所有组件(未选择的组件将被过滤掉并显示已选择的组件)。

4- On the re-creation is done, I would like to scroll to the selected component. 4-在重新创建完成后,我想滚动到选定的组件。

The thing is that when the flipQuantity quantity mutation finishes its execution, I call the scrollToElement callback, but the ref it contains is for the unselected element, that is no longer rendered on the screen, since the new one will be recreated by the SimplesList component.问题是,当flipQuantity数量突变完成执行时,我调用scrollToElement回调,但它包含的 ref 是针对未选择的元素,不再呈现在屏幕上,因为新元素将由SimplesList组件重新创建.

How can I fire the scrollIntoView function on the most up-to-date component?如何在最新组件上触发scrollIntoView function?

UPDATE:更新:

Same code, but with the useRef hook:相同的代码,但使用了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>
  );
}

UPDATE 2:更新 2:

I changed my component once again as per Kornflexx suggestion, but it is still not working:我按照 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>
  );
}

Now I am getting this error:现在我收到此错误:

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.

I've previously solved this by adding a local state flag to items that should be scrolled to when they appear:我以前通过向出现时应滚动到的项目添加本地 state 标志来解决此问题:

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 } });
  }
})

Then when it mounts, I force the scroll and clear the flag:然后当它安装时,我强制滚动并清除标志:

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,
        },
      });
    }
  });

  ...

Doing it this way means that I can completely separate the mutation from the item's rendering, and I can by sure that the scroll will only occur once the item exists in the DOM.这样做意味着我可以将突变与项目的渲染完全分开,并且我可以确定只有当项目存在于 DOM 中时才会发生滚动。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM