Parallax effect with React Spring: How to?

Ever wondered how to create a parallax effect with React Spring? This is one of my favourite effects to use when sprucing up a page with some motion.

However, due to how React works, tracking the scroll position is a bit harder than it would be on a standard website. So let me show you how we can achieve this effect with React Spring, an animation library for React.

Animating React with React Spring

There's a lot to say about React Spring, much more than one article could cover, but in case you're unfamiliar with it, I'll explain the basics.

Because React changes the way the DOM is generated, using jQuery for animation becomes almost impossible. Essentially, using jQuery would require extensive workarounds, which are best avoided.

To avoid wasting precious time fiddling with jQuery, many animation libraries have been developed specifically for React. React Spring is by far my favourite of these.

While it is definitely a lot more complex than jQuery, the possibilities that this library opens are much vaster than what can be achieved with older Javascript-based frameworks. I'll definitely post a more complete introduction to this argument in the future as it's an interesting topic.

Why use React Spring?

On a standard webpage, tracking scrolling position is simple. All we need is an event listener:

window.addEventListener("scroll", function(e) {
  // This will show our scroll position in the console
  console.log(window.scrollY);
});

If you tried this in SSR React however, you'd probably be met with something along the lines of [Error] ReferenceError: window is not defined. This happens because window isn't defined until the first time React renders in pretty much every SSR implementation.

Source and Demo

If you don't have time for the whole explanation or want an example of what we'll end up making, you can find the final results along with all the source in the CodeSandbox below:

How do I do it?

There are a few parts to this solution, so I'll try to go through them and explain what we're doing. Our main objectives here are:

  • Achieving a parallax effect with react spring
  • Without destroying our app's performance

First of all, make sure you import the required react-spring modules:

import { useSpring, animated, interpolate } from "react-spring";

To improve the performance of our animation, we have to make sure the scroll position isn't updated on every frame. To do this we can use the following Debounce function:

export function debounce(func, wait = 5, immediate = false) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    const later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
}

If ran continuously, this will stop execution of the function until a certain amount of time has passed. This allows us to delay when we call the scrolling update to improve performance.

Now, let's set up our main Component containing our page elements:

function App() {
  return (
    <animated.div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Scroll to see how it works!</h2>
    </animated.div>
  );
}

This is pretty standard, but as you might notice we've replaced the containing div with animated.div this is required by React Spring for the animations to work.

Using React hooks, we then set up a state for keeping track of the scroll position:

function App() {
  const [scrollY, setScrollY] = useState(0);

  return (
    <animated.div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Scroll to see how it works!</h2>
    </animated.div>
  );
}

Now to get around the previously mentioned problem of window being undefined. We do this by adding the useEffect hook:

function App() {
  const [scrollY, setScrollY] = useState(0);
  useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);
    window.addEventListener("scroll", debounce(handleScroll));
    return () => window.removeEventListener("scroll", debounce(handleScroll));
  }, [debounce]);

  return (
    <animated.div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Scroll to see how it works!</h2>
    </animated.div>
  );
}

UseEffect ensures that the code inside it is executed only after the component has rendered. It's the hooks equivalent of the old ComponentDidMount.

Inside the useEffect hook we have two things: a constant called handleScroll, as well as addEventListener and removeEventListener.

handleScroll simply sets our variable, making sure our element re-renders to update the changes. The two event listeners on the other hand ensure two things:

  • We have an event listener for scroll every time our element re-renders
  • We do not have more than one scroll event listener at a time

Once that's done we just need to add the react-spring logic to make the animation happen.

function App() {
  const [scrollY, setScrollY] = useState(0);
  useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);
    window.addEventListener("scroll", debounce(handleScroll));
    return () => window.removeEventListener("scroll", debounce(handleScroll));
  }, [debounce]);

  const [{ springscrollY }, springsetScrollY] = useSpring(() => ({
    springscrollY: 0
  }));
  const parallaxLevel = 1;
  springsetScrollY({ springscrollY: scrollY });
  const interpHeader = springscrollY.interpolate(
    o => `translateY(${o / parallaxLevel}px)`
  );

  return (
    <animated.div className="App" style={{ transform: interpHeader }}>
      <h1>Hello CodeSandbox</h1>
      <h2>Scroll to see how it works!</h2>
    </animated.div>
  );
}

Here, we create a spring and we update its values with our scroll. We then use React Spring's interpolate to, well, interpolate our values.

For the animation to actually happen, we then add the interpolated value to our animated.div. Notice how we're dividing the result of the interpolation by parallaxLevel. A value of 1 will cause the div to transform to the exact position on the page you've scrolled to, but I prefer setting to it to around 20, which will cause the element to have a subtle but still noticeable parallax effect.

In conclusion

I had some trouble figuring out how exactly to add this parallax effect with React Spring, but it was worth it in the end. Hopefully, explaining everything here can make it a little easier for others having the same problem I had, and I can save you some precious time!