Skip to content

React strict mode bug #382

@catamphetamine

Description

@catamphetamine

A scroll-behavior instance is created in the constructor() of the <ScrollManager/> component.
According to React strict mode docs, that's an anti-pattern that results in incorrect operation.

  • Component instance A is created
  • Component instance B is created
  • componentDidMount() is called with this being B
  • componentWillUnmount() is called with this being B
  • componentDidMount() is called with this being B

Because of that, clean-up of Component instance A is not run and a scroll event listener with stale props (initial location) responds to scroll events forever.

A fix would be creating a scroll-behavior instance in componentDidMount().

I also rewrote <ScrollManager/> in React hooks in case you're interested (it also contains the fix for the issue):
https://github.com/catamphetamine/react-pages/blob/186eedf4ab427d512e9332926d3d488b307f311f/lib/router/client/found-scroll/ScrollManager.js

I also published a rewrite of farce + scroll-behavior as a new package:
https://www.npmjs.com/package/navigation-stack

Here's the source code for the <ScrollManager/> from the link above copy-pasted:

import { StateStorage } from 'farce';
import React, { useRef, useEffect, useMemo, useCallback, createContext } from 'react';
import ScrollBehavior from 'scroll-behavior';

const STORAGE_NAMESPACE = '@@scroll';

export const ScrollContext = createContext(null);

const defaultCreateScrollBehavior = config => new ScrollBehavior(config);

// Rewrote `<ScrollManager/>` from `found-scroll` in React hooks.
// Also fixed React strict mode bug.
// https://github.com/4Catalyzer/found-scroll/issues/382
export default function ScrollManager({
  renderArgs,
  shouldUpdateScroll: shouldUpdateScrollProperty,
  createScrollBehavior = defaultCreateScrollBehavior,
  children
}) {
  const { router, location } = renderArgs;

  const prevRenderArgs = useRef(null);
  const scrollBehaviorRef = useRef();

  const scrollBehavior = scrollBehaviorRef.current;

  const getCurrentLocation = useCallback(() => {
    return location;
  }, [location]);

  const getCurrentLocationRef = useRef();
  getCurrentLocationRef.current = getCurrentLocation;

  const shouldUpdateScroll = useCallback((prevRenderArgs, renderArgs) => {
    if (!shouldUpdateScrollProperty) {
      return true;
    }
    // A hack to allow access to `ScrollBehavior` internals (e.g. `stateStorage`).
    return shouldUpdateScrollProperty.call(scrollBehavior, prevRenderArgs, renderArgs);
  }, [scrollBehavior]);

  const registerScrollElement = useCallback((key, element) => {
    scrollBehavior.registerElement(key, element, shouldUpdateScroll, renderArgs);
    return () => {
      scrollBehavior.unregisterElement(key);
    };
  }, [
    shouldUpdateScroll,
    scrollBehavior,
    renderArgs
  ]);

  const scrollContext = useMemo(() => ({
    scrollBehavior,
    registerScrollElement
  }), [
    scrollBehavior,
    registerScrollElement
  ]);

  useEffect(() => {
    const scrollBehavior = createScrollBehavior({
      addNavigationListener: router.addNavigationListener,
      stateStorage: new StateStorage(router, STORAGE_NAMESPACE),
      getCurrentLocation: () => getCurrentLocationRef.current(),
      shouldUpdateScroll: (prevRenderArgs, renderArgs) => shouldUpdateScroll(prevRenderArgs, renderArgs)
    });
    scrollBehaviorRef.current = scrollBehavior;
    return () => {
      scrollBehavior.stop();
    }
  }, []);

  useEffect(() => {
    const scrollBehavior = scrollBehaviorRef.current;
    const prevLocation = prevRenderArgs.current && prevRenderArgs.current.location;

    if (renderArgs.location === prevLocation || !(renderArgs.elements || renderArgs.error)) {
      // If the location hasn't actually changed, or if we're in a global
      // pending state, don't update the scroll position.
      return;
    }

    scrollBehavior.updateScroll(prevRenderArgs.current, renderArgs);
    prevRenderArgs.current = renderArgs;
  });

  return React.createElement(ScrollContext.Provider, {
    value: scrollContext
  }, children);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions