2

I am trying to make a one page app where you click the link and it scrolls down to the section corresponding to the menu item. I have spent days researching for a fix that suits my criteria and unfortunately, I am having very little luck.

My criteria is as follows:

  • No external dependencies
  • Must have the url in the address bar (to allow direct links to the particular section)
  • Must not be hacky (i.e Injecting the URL into the address bar)
  • Must be as simple as possible

I hope that isn't asking for too much.

You can have a play around with my CodeSandbox Here. Forks are appreciated!

3
  • 1
    Ok. So. First issue I discovered is that navlink cannot both function and run an onClick. Work arounds 1. Add a subcomponent with an onclick, 2. Handle all Navlink things in onclick (seems like too much). Second issue is using ref in functional components. I'm looking into that now. I think there is a hook for that. I'll post an answer and a fork when I find it. Commented Apr 17, 2020 at 0:13
  • 1
    Ok. You were using the ref hook. But there is a scoping issue. Using it at the header level does little for you in the other levels. Let me play around with this some more. I think Ive almost got it. Commented Apr 17, 2020 at 0:17
  • 1
    I've got to stop for now. codesandbox.io/s/vigorous-microservice-3bqml?file=/src/App.tsx should be the link to onclick working. You've got to play with ref forwarding to get the ref where you need it. reactjs.org/docs/forwarding-refs.html Commented Apr 17, 2020 at 0:24

1 Answer 1

3

You can wrap each section with the forwardRef HOC. Create and set a ref for each section, and pass the refs to the header component so it can call the scrollIntoView function on them.

edit Added an effect to look at the location and trigger scrolling.

const Header = ({ refs }) => {
  const location = useLocation();

  useEffect(() => {
    console.log("location", location.pathname);
    switch (location.pathname) {
      case "/about":
        scrollSmoothHandler(refs.aboutRef);
        break;
      case "/contact":
        scrollSmoothHandler(refs.contactRef);
        break;
      case "/hero":
        scrollSmoothHandler(refs.heroRef);
        break;

      default:
      // ignore
    }
  }, [location, refs]);

  const scrollSmoothHandler = ref => {
    console.log("Triggered.");
    ref.current.scrollIntoView({ behavior: "smooth" });
  };

  return (
    <>
      <NavLink to="/hero" activeClassName="selected">
        Hero
      </NavLink>
      <NavLink to="/about" activeClassName="selected">
        About
      </NavLink>
      <NavLink to="/contact" activeClassName="selected">
        Contact
      </NavLink>
    </>
  );
};

const Hero = forwardRef((props, ref) => {
  return (
    <section ref={ref}>
      <h1>Hero Section</h1>
    </section>
  );
});

const About = forwardRef((props, ref) => {
  return (
    <section ref={ref}>
      <h1>About Section</h1>
    </section>
  );
});

const Contact = forwardRef((props, ref) => {
  return (
    <section ref={ref}>
      <h1>Contact Section</h1>
    </section>
  );
});

function App() {
  const heroRef = useRef(null);
  const aboutRef = useRef(null);
  const contactRef = useRef(null);

  return (
    <div className="App">
      <HashRouter>
        <Header refs={{ aboutRef, contactRef, heroRef }} />
        <Hero ref={heroRef} />
        <About ref={aboutRef} />
        <Contact ref={contactRef} />
      </HashRouter>
    </div>
  );
}

Edit forwardRef and scrollIntoView

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you! That does work, however, it doesn't quite match the criteria. Do you know how to adjust the code so if I typed /contact in the address bar it would scroll down to the contact section?
@Julian Added an effect to trigger scrolling view when location changes. No need for onclick now.
what if I wanted to convert the components to TypeScript? For example: interface IAbout { ref: any } const About: React.FC<IAbout> = forwardRef((ref) => { return ( <section ref={ref}> <h1>About Section</h1> </section> ); }); I get an error when I try this.
@Julian Unfortunately I'm not versed in typescript. A quick google search yielded this: medium.com/@martin_hotell/… With it I was able to remove the warnings/errors in all the components but the Header.
@Julian It could possibly be done as they really only need to be defined in some common ancestor. In the demo App just happens to be the closest (and only) common ancestor. IMO it makes sense to define them in the component holding the router/routes as that component is the one likely to have the best picture of the routes or elements that need to be scrolled to.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.