Colin Sygiel
Colin Sygiel

Reputation: 947

Scroll page to child React component on a button click

I have a parent component that renders a child component below the header. The header has a button that, when clicked, should scroll to a section of the corresponding child component.

Right now I am trying to change the state on the button click, and pass this new state as a prop from the parent to the child component. Then, in the child component, I am trying to run the scroll() function using the passed down prop as the identifier of the section I want to scroll to.

Parent component:

 handleSelected = event => {
  this.setState({ selected: event.currentTarget.value });
 };

 <Button value="sectionOne" onClick={this.handleSelected} />

 <Child loadSelected={() => this.state.selected}</Child>

Child component:

  ComponentDidMount() {
   let selected = this.props.loadSelected();
   this.handleNavigate(selected);
  }

  handleNavigate = section => {
    let el = document.getElementById(section);
    window.scrollTo({
      behavior: "smooth",
      left: 0,
      top: el.offsetTop
    });
  };
  ....
  ....
  ....
  <div id="sectionOne" />

Upvotes: 1

Views: 7244

Answers (1)

Dacre Denny
Dacre Denny

Reputation: 30360

When interacting directly with elements in the DOM, you'll typically want to do so via refs, rather than by elements queried from the DOM with methods such as getElementById(), querySelectorAll(), etc.

There are a number of ways you can integrate refs into your application to achieve what you require - one approach would be to create a ref per section, store each section with corresponding ref in the parent (Header?) component, and then handleNavigate() from the parent rather than from the child as shown here:

class Parent extends React.Component {

  constructor(props) {
    super(props)
        
    /* Mock sections data */
    this.sections = [
    {
      name : "Foo",
      color: "pink",
      ref : React.createRef() /* Ref per section */
    },
    {
      name : "Bar",
      color: "lightblue",
      ref : React.createRef()
    },
    {
      name : "Cat",
      color: "lightgreen",
      ref : React.createRef()
    }];
  }
  
  /* Move into parent/header */
  handleNavigate = section => {
    
    /* 
    Access the "current element" of this sections ref. 
    Treat this as the element of the div for this section.
    */
    let el = section.ref.current;
    
    window.scrollTo({
      behavior: "smooth",
      left: 0,
      top: el.offsetTop
    });
  };
  
  render() {
    return (
        <div>
        <nav>
        <h2>Menu</h2>
        { 
            this.sections.map(section => 
            <button onClick={() => this.handleNavigate(section)}>
            {section.name}
            </button>)
        }
        </nav>
        <div>
        { this.sections.map(section => <Child section={section} />)}
        </div>
      </div>
    )
  }
}

class Child extends React.Component {
    render() {
    
    const { section } = this.props;
    const style = { 
        padding:"15rem 0", background : section.color 
    };
    
    return <div ref={ section.ref } style={ style }>
        Section for { section.name }
    </div>
  }
}

Refs provide a reliable way of interacting with the real DOM element that represents part of the rendered result of a component. In the code above, a ref is associated with the div of a Child. The ref's current field provides access to the actual DOM element of that child's div, which can be passed to the scrollTo() method to achieve the smooth scroll animation you're after.

For a working example of this approach, see this fiddle. Hope that helps!

Upvotes: 4

Related Questions