【问题标题】:How to scroll a React page when it loads?加载时如何滚动 React 页面?
【发布时间】:2019-08-13 15:40:35
【问题描述】:

我想在 url (myapp.com#somevalue) 中传递一个值作为哈希值,并在页面加载时让页面滚动到该项目 - 这正是自互联网开始以来使用哈希片段的行为。我尝试使用 scrollIntoView 但在 iOS 上失败了。然后我尝试取消设置/设置 window.location.hash 但似乎存在竞争条件。仅在延迟超过 600ms 时有效。

我想要一个更可靠的解决方案,并且不想引入不必要的延迟。当延迟太短时,它似乎滚动到所需的项目,但随后滚动到页面顶部。您不会在此演示中看到效果,但它发生在我的实际应用程序 https://codesandbox.io/s/pjok544nrx 第 75 行

  componentDidMount() {
    let self = this;

    let updateSearchControl = hash => {
      let selectedOption = self.state.searchOptions.filter(
        option => option.value === hash
      )[0];
      if (selectedOption) {
        // this doesn't work with Safari on iOS
        // document.getElementById(hash).scrollIntoView(true);

        // this works if delay is 900 but not 500 ms
        setTimeout(() => {
          // unset and set the hash to trigger scrolling to target
          window.location.hash = null;
          window.location.hash = hash;
          // scroll back by the height of the Search box
          window.scrollBy(
            0,
            -document.getElementsByClassName("heading")[0].clientHeight
          );
        }, 900);
      } else if (hash) {
        this.searchRef.current.select.focus();
      }
    };

    // Get the hash
    // I want this to work as a Google Apps Script too which runs in
    // an iframe and has a special way to get the hash
    if (!!window["google"]) {
      let updateHash = location => {
        updateSearchControl(location.hash);
      };
      eval("google.script.url.getLocation(updateHash)");
    } else {
      let hash = window.location.hash.slice(1);
      updateSearchControl(hash);
    }
  }

编辑:我追踪了重新渲染页面的 React 行,因此在它已经滚动到我告诉它进入 componentDidMount() 的位置后重置了滚动位置。这是this onestyle[styleName] = styleValue; 在重新渲染页面时,它正在设置反应选择框组件的输入框组件的宽度样式属性。执行此操作之前的堆栈跟踪看起来像

(anonymous) @   VM38319:1
setValueForStyles   @   react-dom.development.js:6426
updateDOMProperties @   react-dom.development.js:7587
updateProperties    @   react-dom.development.js:7953
commitUpdate    @   react-dom.development.js:8797
commitWork  @   react-dom.development.js:17915
commitAllHostEffects    @   react-dom.development.js:18634
callCallback    @   react-dom.development.js:149
invokeGuardedCallbackDev    @   react-dom.development.js:199
invokeGuardedCallback   @   react-dom.development.js:256
commitRoot  @   react-dom.development.js:18867
(anonymous) @   react-dom.development.js:20372
unstable_runWithPriority    @   scheduler.development.js:255
completeRoot    @   react-dom.development.js:20371
performWorkOnRoot   @   react-dom.development.js:20300
performWork @   react-dom.development.js:20208
performSyncWork @   react-dom.development.js:20182
requestWork @   react-dom.development.js:20051
scheduleWork    @   react-dom.development.js:19865
scheduleRootUpdate  @   react-dom.development.js:20526
updateContainerAtExpirationTime @   react-dom.development.js:20554
updateContainer @   react-dom.development.js:20611
ReactRoot.render    @   react-dom.development.js:20907
(anonymous) @   react-dom.development.js:21044
unbatchedUpdates    @   react-dom.development.js:20413
legacyRenderSubtreeIntoContainer    @   react-dom.development.js:21040
render  @   react-dom.development.js:21109
(anonymous) @   questionsPageIndex.jsx:10
./src/client/questionsPageIndex.jsx @   index.html:673
__webpack_require__ @   index.html:32
(anonymous) @   index.html:96
(anonymous) @   index.html:99

我不知道将滚动页面的指令移到哪里。它必须在设置这些样式之后发生,这发生在 componentDidMount() 之后。

编辑:我需要更好地澄清我需要这样做。很抱歉之前没有这样做。

必备

这必须适用于所有常见的桌面和移动设备。

当页面加载时,可能会出现三种情况,具体取决于 #: 后 url 中提供的查询:

  • 1) 未提供查询 - 没有任何反应
  • 2) 提供了有效查询 - 页面立即滚动到所需的锚点,并且页面标题更改为选项标签
  • 3) 提供了无效的查询 - 查询被输入到搜索框中,搜索框被聚焦并且菜单打开,其中的选项受所提供的查询限制,就好像它们已被输入一样。光标位于查询末尾的搜索框,以便用户修改查询。

有效的查询是与其中一个选项的值匹配的查询。无效查询是无效查询。

浏览器的后退和前进按钮应该相应地移动滚动位置。

每当从搜索框中选择一个选项时,页面应立即滚动到该锚点,查询应清除返回占位符“搜索...”,网址应更新,文档标题应更改为选项的标签。

可选但会很棒

选择后,菜单关闭,查询返回“搜索...”,然后

  • “搜索”框保留焦点,以便用户可以开始键入另一个查询。下一次单击时,菜单打开,并且选择之前的搜索框查询、菜单的过滤器和菜单滚动位置被恢复(最优选),或者
  • 搜索框失去焦点。下一次聚焦时,菜单打开,搜索框查询从选择之前、菜单的过滤器和菜单滚动位置恢复(首选),或
  • “搜索”框保留焦点,以便用户可以开始键入另一个查询。先前的查询和菜单滚动位置丢失。

【问题讨论】:

  • 可能的解决方案 - stackoverflow.com/questions/3163615/…
  • 也许 useEffect 值得一试。据我了解,useEffect 将在渲染提交到屏幕后运行,此时元素已准备好滚动到

标签: reactjs react-select


【解决方案1】:

我把它放在组件文件中的导入之后:

let initialValidSlug = '';

window.addEventListener('load', () => {
  window.location.hash = '';
  window.location.hash = (initialValidSlug ? initialValidSlug : '');
  window.scrollBy(0, -document.getElementsByClassName("heading")[0].clientHeight);
  console.log('window.addEventListener')
});

把这个送给我的componentDidMount():

  componentDidMount() {
    const { pathname, isValid } = this.state;
    if(!isValid && pathname) {               // if there's a pathname but it's invalid,
      this.searchRef.current.select.focus(); // focus on the search bar
    }
    initialValidSlug = pathname;  // sets the value to be used in the $.ready() above
  }

【讨论】:

    【解决方案2】:

    您可以通过将react-router-domHashRouter 与React 的ref.current.scrollIntoView()window.scrollTo(ref.currrent.offsetTop) 一起使用来简化您的代码。

    目前已在 Chrome(桌面/安卓)、Firefox(桌面)、Silk Browser(安卓)、Saumsung Internet(安卓)和 Safari(iOS - 有一个警告是它会立即跳转到选择)上进行测试和工作.虽然我能够测试并确认它在沙盒环境的 iframe 中工作,但您必须在独立的 iframe 中调整/测试它。

    我还想指出,您的方法非常类似于jQuery,您应该避免直接尝试使用或操纵DOM 和/或window。此外,您似乎在整个代码中使用了很多let 变量,但它们保持不变。相反,它们应该是 const 变量,因为 React 每次重新渲染时都会评估变量,因此除非您计划在重新渲染过程中更改该变量,否则无需使用 let

    工作示例https://v6y5211n4l.codesandbox.io


    组件/滚动条

    import React, { Component } from "react";
    import PropTypes from "prop-types";
    import Helmet from "react-helmet";
    import Select from "react-select";
    import { withRouter } from "react-router-dom";
    import "./styles.css";
    
    const scrollOpts = {
      behavior: "smooth",
      block: "start"
    };
    
    class ScrollBar extends Component {
      constructor(props) {
        super(props);
    
        const titleRefs = props.searchOptions.map(
          ({ label }) => (this[label] = React.createRef())
        ); // map over options and use "label" as a ref name; ex: this.orange
        this.topWindow = React.createRef();
        this.searchRef = React.createRef();
        const pathname = props.history.location.pathname.replace(/\//g, ""); // set current pathname without the "/"
    
        this.state = {
          titleRefs,
          menuIsOpen: false,
          isValid: props.searchOptions.some(({ label }) => label === pathname), // check if the current pathname matches any of the labels
          pathname
        };
      }
    
      componentDidMount() {
        const { pathname, isValid } = this.state;
        if (isValid) this.scrollToElement(); // if theres a pathname and it matches a label, scroll to it
        if (!isValid && pathname) this.searchRef.current.select.focus(); // if there's a pathname but it's invalid, focus on the search bar
      }
    
      // allows us to update state based upon prop updates (in this case
      // we're looking for changes in the history.location.pathname)
      static getDerivedStateFromProps(props, state) {
        const nextPath = props.history.location.pathname.replace(/\//g, "");
        const isValid = props.searchOptions.some(({ label }) => label === nextPath);
        return state.pathname !== nextPath ? { pathname: nextPath, isValid } : null; // if the path has changed, update the pathname and whether or not it is valid
      }
    
      // allows us to compare new state to old state (in this case
      // we're looking for changes in the pathname)
      componentDidUpdate(prevProps, prevState) {
        if (this.state.pathname !== prevState.pathname) {
          this.scrollToElement();
        }
      }
    
      scrollToElement = () => {
        const { isValid, pathname } = this.state;
        const elem = isValid ? pathname : "topWindow";
        setTimeout(() => {
         window.scrollTo({
            behavior: "smooth",
            top: this[elem].current.offsetTop - 45
          });
        }, 100);
      };
    
      onInputChange = (options, { action }) => {
        if (action === "menu-close") {
          this.setState({ menuIsOpen: false }, () =>
            this.searchRef.current.select.blur()
          );
        } else {
          this.setState({ menuIsOpen: true });
        }
      };
    
      onChange = ({ value }) => {
        const { history } = this.props;
        if (history.location.pathname.replace(/\//g, "") !== value) { // if the select value is different than the previous selected value, update the url -- required, otherwise, react-router will complain about pushing the same pathname/value that is current in the url
          history.push(value);
        }
        this.searchRef.current.select.blur();
      };
    
      onFocus = () => this.setState({ menuIsOpen: true });
    
      render() {
        const { pathname, menuIsOpen, titleRefs } = this.state;
        const { searchOptions, styles } = this.props;
    
        return (
          <div className="container">
            <Helmet title={this.state.pathname || "Select an option"} />
            <div className="heading">
              <Select
                placeholder="Search..."
                isClearable={true}
                isSearchable={true}
                options={searchOptions}
                defaultInputValue={pathname}
                onFocus={this.onFocus}
                onInputChange={this.onInputChange}
                onChange={this.onChange}
                menuIsOpen={menuIsOpen}
                ref={this.searchRef}
                value={null}
                styles={styles || {}}
              />
            </div>
            <div className="fruits-list">
              <div ref={this.topWindow} />
              {searchOptions.map(({ label, value }, key) => (
                <div key={value} id={value}>
                  <p ref={titleRefs[key]}>{label}</p>
                  {[1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14].map(num => (
                    <p key={num}>{num}</p>
                  ))}
                </div>
              ))}
            </div>
          </div>
        );
      }
    }
    
    ScrollBar.propTypes = {
      searchOptions: PropTypes.arrayOf(
        PropTypes.shape({
          label: PropTypes.string.isRequired,
          value: PropTypes.string.isRequired
        }).isRequired
      ).isRequired,
      styles: PropTypes.objectOf(PropTypes.func)
    };
    
    export default withRouter(ScrollBar);
    

    index.js

    import React from "react";
    import { render } from "react-dom";
    import { HashRouter } from "react-router-dom";
    import Helmet from "react-helmet";
    import ScrollBar from "../src/components/ScrollBar";
    
    const config = {
      htmlAttributes: { lang: "en" },
      title: "My App",
      titleTemplate: "Scroll Search - %s",
      meta: [
        {
          name: "description",
          content: "The best app in the world."
        }
      ]
    };
    
    const searchOptions = [
      {
        label: "apple",
        value: "apple"
      },
      {
        label: "orange",
        value: "orange"
      },
      {
        label: "banana",
        value: "banana"
      },
      {
        label: "strawberry",
        value: "strawberry"
      }
    ];
    
    const styles = {
      menu: base => ({ ...base, width: "100%", height: "75vh" }),
      menuList: base => ({ ...base, minHeight: "75vh" }),
      option: base => ({ ...base, color: "blue" })
    };
    
    const App = () => (
      <HashRouter>
        <Helmet {...config} />
        <ScrollBar searchOptions={searchOptions} styles={styles} />
      </HashRouter>
    );
    
    render(<App />, document.getElementById("root"));
    

    【讨论】:

    • 太好了,谢谢。它可以很好地处理提供有效值的情况,但我也想处理无效值。我澄清了上面的细节并开始修改您的解决方案codesandbox.io/s/xl46pnmr8p
    • 更新了上面的答案。此外,移动浏览器需要setTimeout。否则,它不会滚动。
    • 刚刚完成。它适用于我的电脑,但它不适用于我 iPhone 上的 Safari。它会在加载时滚动到正确的位置,但搜索框会消失。如果您手动一直向上滑动,然后再滑动一次,则该框会返回。如果您手动滚动但 scrollIntoView() 在此设备上不起作用,搜索框将保持不变。请在手机上试试这个:v6y5211n4l.codesandbox.io/#/orange
    • 更新了答案。这是移动设备不喜欢body 上的overflow: hidden;Select 组件上的position: absolute; 的问题。相反,将.heading 设置为position: fixed; width: 100%; 并删除overflow: hidden;。因此,您必须使用window.scrollTo()offsetTop 计算,这将滚动到正确的位置(由于固定位置浮动在上方,您必须偏移它,否则它将直接降落在ref 的顶部)。此外,另一个缺点是使用back 按钮时会立即跳转到最后选择的位置。
    • 无论您选择哪个方向,ref.scrollIntoView()window.scrollTo() 您都会遇到浏览器限制。
    【解决方案3】:

    使用react-scrollable-anchor 库可能更容易做到这一点:

    import React from "react";
    import ReactDOM from "react-dom";
    import ScrollableAnchor from "react-scrollable-anchor";
    
    class App extends React.Component {
      render() {
        return (
          <React.Fragment>
            <div style={{ marginBottom: 7000 }}>nothing to see here</div>
            <div style={{ marginBottom: 7000 }}>never mind me</div>
            <ScrollableAnchor id={"doge"}>
              <div>bork</div>
            </ScrollableAnchor>
          </React.Fragment>
        );
      }
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    

    你可以去这里查看它的工作原理:https://zwoj0xw503.codesandbox.io/#doge

    【讨论】:

    • 在内部,ScrollableAnchors 通过设置 window.location.hash 来影响滚动,因此它对 What 问题的答案相同,但不回答 When 问题。如果这确实是 TC39 的问题,并且鉴于我没有立即收到任何即时答案,即无法捕获“准备好滚动的事件”,那么我第一次肯定违反了网页设计的基本原则项目,但这真的很难接受。这似乎是一个非常合理的要求。 github.com/gabergg/react-scrollable-anchor/blob/master/src/…
    【解决方案4】:

    您可以使用https://www.npmjs.com/package/element.scrollintoviewifneeded-polyfill

    尽可能在您的应用程序中导入库

    import 'element.scrollintoviewifneeded-polyfill';
    

    你可以在所有 dom 节点上使用 scrollIntoViewIfNeeded(); 方法。 该库将填充该方法,以便您可以在所有浏览器上使用它。

    我建议您将其与 react ref 一起使用,这样您就不必调用 getElementById 或其他文档方法来检索 dom 节点。

    const ref = React.createRef();
    <div id="somevalue" ref={ref} />
    

    componentDidMount 相比,您可以使用您描述的相同方法来检索要滚动的 div 的 id,获取关联的 ref 并调用

        ref.current.scrollIntoViewIfNeeded();
    

    如果 ref 是正确的 div 引用,您显然可以将所有 ref 存储在地图中,而不是在您的组件方法中检索。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-15
    • 2023-04-05
    • 2018-05-22
    • 2015-02-26
    • 2012-10-05
    相关资源
    最近更新 更多