簡體   English   中英

渲染后將滾動反應到元素

[英]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- 應用程序 state 已更新並重新創建列表中的所有組件(未選擇的組件將被過濾掉並顯示已選擇的組件)。

4-在重新創建完成后,我想滾動到選定的組件。

問題是,當flipQuantity數量突變完成執行時,我調用scrollToElement回調,但它包含的 ref 是針對未選擇的元素,不再呈現在屏幕上,因為新元素將由SimplesList組件重新創建.

如何在最新組件上觸發scrollIntoView function?

更新:

相同的代碼,但使用了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.

我以前通過向出現時應滾動到的項目添加本地 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 } });
  }
})

然后當它安裝時,我強制滾動並清除標志:

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 中時才會發生滾動。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM