Nomas Prime
Nomas Prime

Reputation: 1344

Is there a better way to call ref.current.scrollIntoView() from a child component to scroll to a ref in its parent?

I'm working on my first ReactJS/TypeScript project.

All I want to do is call ref.current.scrollIntoView() from a child component to scroll to a ref in its parent.

I've got it working using a fairly ugly if statement in the onClick handler, scrollTest(), to keep TypeScript happy. Hoping there's a cleaner way of doing this.

Bonus points if there's a way to avoid setting Sections.displayname explicitly.

import {
  Container,
  Grid,
  Link,
  Typography
} from '@material-ui/core'

import React, { ReactElement } from 'react'

const Section = (
  {
    children
  }: {
    children: React.ReactNode
  }
): ReactElement => {
  return(
    <>
      <Grid container style={{minHeight: "100vh"}}>
        <Grid item>
          {children}
        </Grid>
      </Grid>
    </>
  )
}

const Sections = React.forwardRef<HTMLElement>((_, ref): ReactElement => {
  const scrollTest = (): void => {
    if (ref && "current" in ref && ref.current) {
      ref.current.scrollIntoView()
    }
  }

  const sections = [
    <>
      <Typography paragraph variant="h3">
        <Link onClick={scrollTest}>Scroll to footer</Link>
      </Typography>
    </>
  ].map((section, index) => {
    return(
      <Section key={index}>
        {section}
      </Section>
    )
  })

  return(<>{sections}</>)
})

Sections.displayName = 'Sections'

export default function Test(): ReactElement {
  const footerRef = React.useRef<HTMLElement>(null)

  return (
    <>
      <Container>
        <Sections ref={footerRef} />
        <Grid
          component="footer"
          container
          ref={footerRef}
        >
          <Grid>
            <Typography paragraph>
              Footer
            </Typography>
          </Grid>
        </Grid>
      </Container>
    </>
  )
}

Upvotes: 0

Views: 1114

Answers (1)

Nomas Prime
Nomas Prime

Reputation: 1344

Based on jtbandes’ comments I got the following:

import {
  Container,
  Grid,
  Link,
  Typography
} from '@material-ui/core'

import React, { ReactElement } from 'react'

const Section = (
  {
    children
  }: {
    children: React.ReactNode
  }
): ReactElement => {
  return(
    <>
      <Grid container style={{minHeight: "100vh"}}>
        <Grid item>
          {children}
        </Grid>
      </Grid>
    </>
  )
}

const Sections = (
  {
    footerRef
  }: {
    footerRef: React.RefObject<HTMLElement>
  }
): ReactElement => {
  const scrollToFooter = (): void => {
      footerRef?.current?.scrollIntoView()
  }

  const sections = [
    <>
      <Typography paragraph variant="h3">
        <Link onClick={scrollToFooter}>Scroll to footer</Link>
      </Typography>
    </>
  ].map((section, index) => {
    return(
      <Section key={index}>
        {section}
      </Section>
    )
  })

  return(<>{sections}</>)
}

export default function Test(): ReactElement {
  const footerRef = React.useRef<HTMLElement>(null)

  return (
    <>
      <Container>
        <Sections footerRef={footerRef} />
        <Grid
          component="footer"
          container
          ref={footerRef}
        >
          <Grid>
            <Typography paragraph>
              Footer
            </Typography>
          </Grid>
        </Grid>
      </Container>
    </>
  )
}

Don't know why but without forwardRef() the safe operator, ?, now works with both the RefObject and current making the if statement redundant.

Removing forwardRef() also removes the need to set displayname explicitly, bonus points given.

Considered Dennis Matinez’s suggestion but it made more sense to me to have the click handler in the same component as the source of the click event.

Upvotes: 1

Related Questions