skip to Main Content

I’m following this part of this tutorial

(Edit: created a github project showing the issue here, diff behaviors if running dev or prod modes)

(Edit 2: sandbox project showing the differences when running "npm run dev" vs "npm run build + npm start" here)

I managed to implement the desired functionality of the scrolling line and the closing circles.
But when deployed to production, the circles seem to be using all the same ref, as they’re all synchronized instead of completing independently as the user scrolls down.
Added an imagine placing the behavior side by side to better illustrate whats happening. Again, this only happens when running a production build of the project

enter image description here

Here’s the relevant code of the circles

ExperienceDetail.tsx

import React, { useRef } from 'react';
import { Job } from './jobList';
import styles from './ExperienceDetail.module.css';
import ListIcon from './ListIcon';
import { motion } from 'framer-motion';

interface DetailsProps {
  job: Job;
}

const ExperienceDetail: React.FC<DetailsProps> = ({ job }) => {
  const ref = React.createRef<HTMLElement>();
  return (
    <li
      ref={ref as any}
      className={`${styles.detailContainer}`}
      id={job.position}
    >
      <ListIcon reference={ref} />
      <motion.div
        initial={{ y: 50 }}
        whileInView={{ y: 0 }}
        transition={{ duration: 0.618, type: 'spring' }}
      >
        <h3 className={`${styles.title} is-capitalized has-text-weight-bold `}>
          {job.position}&nbsp;
          <a
            href={job.companyLink}
            target="_blank"
            className="has-text-primary-dark is-capitalized "
          >
            @{job.company}
          </a>
        </h3>
        <span className="is-capitalized has-text-weight-medium has-text-grey">
          {job.time} | {job.location}
        </span>
        <p
          className={'mt-2 has-text-weight-medium'}
          dangerouslySetInnerHTML={{ __html: job.description }}
        />
      </motion.div>
    </li>
  );
};

export default ExperienceDetail;

ListIcon.tsx

import React from 'react';
import styles from './ListIcon.module.css';
import { motion, useScroll } from 'framer-motion';

interface ListIconProps {
  reference: React.MutableRefObject<HTMLElement | null>;
}

const ListIcon: React.FC<ListIconProps> = ({ reference }) => {
  const { scrollYProgress } = useScroll({
    target: reference,
    offset: ['center end', 'center center'],
  });
  return (
    <figure className={styles.listIcon}>
      <svg width="75" height="75" viewBox="0 0 100 100">
        <circle cx="75" cy="50" r="20" className={styles.circle1} />
        <motion.circle
          cx="75"
          cy="50"
          r="20"
          className={styles.circle2}
          style={{ pathLength: scrollYProgress }}
        />
        <circle
          cx="75"
          cy="50"
          r="10"
          className={`${styles.circle3} ${styles.pulse}`}
        />
      </svg>
    </figure>
  );
};

export default ListIcon;

The ExperienceDetail component is being rendered by a map function on a parent component like this

<ul>
    {list.map((item, index) => (
        <ExperienceDetail item={item} key={`${type}-${index}`} />
    ))}
</ul>

I’ve tried undoing the map and rendering them directly, but no success.
Also tried swapping the useRef for React.createRef but again, nothing.

What could be causing this difference in behavior between local and production builds?

I’ve tried undoing the map and rendering them directly, but no success. Also tried swapping the useRef for React.createRef but again, nothing.

2

Answers


  1. Chosen as BEST ANSWER

    Solved the issue by passing scrollYProgress to the LiIcon component instead of passing the ref

    const Details = ({ position, company, time, address, work }) => {
      const ref = useRef(null);
    
      const { scrollYProgress } = useScroll({
        target: ref,
        offset: ['center end', 'center center'],
      });
    
      return (
        <li
          ref={ref}
          className="my-8 first:mt-0 last:mb-8 w-[60%] mx-auto flex flex-col items-center justify-between"
        >
          <LiIcon scrollYProgress={scrollYProgress} />
    

    More info: https://github.com/vercel/next.js/discussions/60745


  2. I suspect that because the ref variable is passed as a prop, React is not re-rendering the <ListIcon> Element.

    On first render, the ref is instantiated, its current value is null.

    useScroll falls back to track the documentElement when the target is null.

    Later, the ref.current is updated with the <li> element, but React has no reason to re-render <ListIcon> because the only prop reference={ref} hasn’t changed.

    Remember that refs are stable references, and React only does a shallow comparison of props. So, <ListIcon> doesn’t rerender when ref.current is set.

    The useScroll hook in framer-motion adds the target prop as the useEffect dependencies, but again, refs are stable references. So the useEffect has no reason to update.

    https://github.com/framer/motion/blob/main/packages/framer-motion/src/value/use-scroll.ts

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search