在React移动应用程序中保持和恢复滚动位置

853 阅读7分钟

在移动网站上添加导航菜单时,一个常见的方法是让移动菜单占据整个页面,并将原页面隐藏在其下方。

但是,这样做我们经常面临的一个问题是,当导航菜单打开并覆盖它时,下面的页面必须仍然可以滚动。这个问题会产生不好的用户体验,我们希望不惜一切代价避免这种情况。

解决这个问题的一个方法是,在我们打开菜单的原始页面上添加position: fixed 。虽然这样做可以使页面不再滚动,但通过解决这个问题,我们引入了另一个问题。

当我们打开菜单时,在页面上添加position: fixed ,当我们关闭菜单时,再把它改回position: relative ,我们就失去了页面当前的滚动位置,用户就会返回到页面的顶部,这又是一个糟糕的用户体验。

所以,为了解决这两个问题,我们将学习如何在React应用程序中实现一个自定义的Hook,让我们在全页面移动菜单打开时停止页面滚动。

但是,最重要的是,当我们关闭全页面移动菜单时,我们会保持滚动的位置,这样用户就可以从他们离开的地方继续滚动了。

前台模板化

为了开始学习本教程,我们需要使用create-react-app (如果你以前没有使用过它,可以在这里了解更多信息)创建一个React应用程序。

一旦我们创建了新的React应用程序并安装了所有的依赖项,我们就可以运行npm run start 来启动我们的React应用程序。

随着我们的应用程序的启动,让我们建立一个快速的前端,使我们能够展示我们将在稍后建立的自定义Hook。下面是你需要放到./src/App.js 文件中的代码。

import { useEffect, useState } from 'react';
import Nav from './components/Nav';
import './App.css';

function App() {
  const [scrollValue, setScrollValue] = useState(0);

  useEffect(() => {

    const onScroll = (e) => {
      setScrollValue(e.target.documentElement.scrollTop);
    };

    window.addEventListener('scroll', onScroll);

    return () => window.removeEventListener('scroll', onScroll);
  }, [scrollValue]);

  return (
    <div className="App">
      <p className="filler" />
      <div className="fixed">
        <p>Current Scroll Position:</p>
        <p>{scrollValue}px</p>
      </div>
    </div>
  );
}
export default App;

如果你以前看过React应用程序,这些应该都很熟悉,所以我们不会涉及整个文件的代码。

然而,我想触及的一块是useEffect() 块中的代码。这段代码允许我们将当前的滚动位置置于状态中,然后将其显示在页面上。虽然这不是自定义Hook工作的必要条件,但它显示了自定义Hook在我们的应用程序中的作用。

这是我们的App.js 文件的样式,它存储在同一目录下的App.css

.App {
  padding: 0 5rem;
}
.filler {
  height: 300vw;
}
.fixed {
  text-align: center;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-weight: 700;
  font-size: 45px;
}
.fixed > * {
  margin: 0;
}

现在你应该有一个允许你向上和向下滚动的页面,并在页面上显示当前的滚动位置,像这样。

Scrollable Page With Current Scroll Position

有了这个实现,我们只需要创建我们的移动导航菜单。现在让我们通过向我们的应用程序添加./src/components/Nav.js ,来创建这个。下面是这个新组件的代码。

import './Nav.css';

export default function Nav() {

  return (
    <nav>
      {/* We will replace this true boolean when we create the hook as this will contain the open/closed state for the nav menu.*/}
      {true ? (
        {/* We will add a onClick handler after we have defined the hook */}
        <button type="button">
          Open Menu
        </button>
      ) : (
        <div>
          <p>Nav Item 1</p>
          <p>Nav Item 2</p>
          <p>Nav Item 3</p>

          {/* We will add a onClick handler after we have defined the hook */}
          <button type="button">
            Close Menu
          </button>
        </div>
      )}
    </nav>
  );
}

添加了这段代码后,我们现在应该在页面的标题和当前滚动位置之间添加一个打开菜单按钮。目前,如果我们点击这个按钮,什么也不会发生,因为我们还没有添加一个onClick 处理程序。

我们还没有添加处理程序的原因是,菜单打开或关闭的状态将包含在我们将在下一节创建的自定义Hook中。

一旦我们创建了自定义Hook,我们将返回到这个组件,并添加相关的代码和样式,以允许移动导航菜单的打开和关闭。

创建我们的useMenuControl 自定义Hook

现在我们进入了本文的主要部分:创建自定义Hook,它将控制整页移动菜单的打开和关闭,并保持其下方页面的滚动位置。

下面是useMenuControl 自定义Hook的代码;要将其添加到应用程序中,我们必须为Hook创建一个新的文件:./src/hooks/useMenuControl.js

import { useEffect, useState } from 'react';

export default function useMenuControl() {
  // State to record if the menu is open
  const [isMenuOpen, setMenuOpen] = useState(false);

  // Click handler for opening and closing the menu
  const clickHandler = () => setMenuOpen(!isMenuOpen);

  useEffect(() => {
    // Get original body overflow style so we can revert to it later on
    const originalStyle = window.getComputedStyle(document.body).overflow;

    // If the menu is open then set the overflow to hidden to prevent scrolling the page further
    if (isMenuOpen) {
      document.body.style.overflow = 'hidden';
    }

    // Re-enable scrolling when component unmounts by reverting to the original body overflow style value
    return () => {
      document.body.style.overflow = originalStyle;
    };
  }, [isMenuOpen]);

  // Returning 3 vales :
  // 1. isMenuOpen: boolean - Is the menu open or not.
  // 2. clickHandler: function - Function used to open and close the menu
  return { isMenuOpen, clickHandler };
}

代码中注释了整个Hook所发生的事情,所以我不会涵盖整个代码,但需要关注的关键部分是文件顶部的状态,我们用它来控制菜单是打开还是关闭。

然后是我们的clickHandler 函数,我们将把它传递给我们之前的Nav 组件中的onClick 属性。

但是,最重要的是 "块 "内的代码。 [useEffect()](https://blog.logrocket.com/guide-to-react-useeffect-hook/) 内的代码。在我们打开菜单之前,这段代码获得了文档主体的当前overflow 样式(默认为visible )。然后,如果isMenuOpen 的状态是true ,我们就将主体的overflow 设置为hidden

这是重要的部分,因为通过将overflow 改为隐藏,我们可以防止页面的滚动,当我们将overflow 恢复到原始值(visible)时,滚动位置仍然是我们打开页面时的位置。

通过避免使用position: fixed 来防止全页移动菜单打开时的滚动,我们就不会失去滚动位置。

现在,让我们来看看在之前的Nav 组件中实现我们的新useMenuControl 钩子。

实现useMenuControl 自定义钩子

现在让我们重新审视我们之前的Nav 组件,并添加上一节中的useMenuControl 自定义Hook。下面是添加了Hook后的更新代码。

import useMenuControl from '../hooks/useMenuControl';
import './Nav.css';

export default function Nav() {
  const { isMenuOpen, clickHandler } = useMenuControl();

  return (
    <nav className={`${isMenuOpen ? 'menuOpen' : ''}`}>
      {!isMenuOpen ? (
        <button type="button" onClick={clickHandler}>
          Open Menu
        </button>
      ) : (
        <div>
          <p>Nav Item 1</p>
          <p>Nav Item 2</p>
          <p>Nav Item 3</p>
          <button type="button" onClick={clickHandler}>
            Close Menu
          </button>
        </div>
      )}
    </nav>
  );
}

这里添加的关键代码是我们用来控制菜单是否显示的isMenuOpen 状态,以及.menuOpen 类是否被应用到nav 元素。

这一点很重要,因为一会儿,我们将为这个类名添加样式,使菜单成为一个真正的全页菜单。

其次,我们从自定义Hook中引入我们的clickHandler ,并将其传递给我们按钮上的两个onClick 处理程序,以控制页面上的菜单的打开和关闭。

有了这个分类,我们只需要通过添加一个./src/components/Nav.css 文件来为.menuOpen 类添加一些样式。下面是我们需要添加的样式,以创建全页面的菜单。

.menuOpen {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2.5rem;
  background-color: white;
  opacity: 1;
  height: 100vh;
  width: 100vw;
}

结论

随着最后一段代码添加到我们的应用程序中,我们现在有了一个React应用程序,它允许我们打开一个全页面的移动菜单,在菜单下保持页面的滚动位置,同时也防止在菜单打开时滚动页面。

下面是这个应用程序的最终版本的一瞥;如果你有兴趣查看这个项目的代码,你可以在我的GitHub上看到它。

Final Version Of Application With Scroll

我希望你觉得这篇关于创建一个自定义React Hook以保持全页移动菜单的滚动位置的文章对你有帮助。

如果你觉得有帮助,请考虑在Twitter上关注我,我在那里发布关于JavaScript生态系统和整个Web开发的有用和可操作的提示和内容。或者,如果你不喜欢Twitter,请访问我的博客,了解我的更多内容。

The post Maintain and restore scroll position in React mobile appsappeared first onLogRocket Blog.