reactjsreduxreact-routerrenderingscrollto

Is it possible to activate a scrollTo before React renders its components?


In my React application, I have a TextHistory component which displays the save history of a given text in a small wrapper. React Router permits me to access every version separately (localhost:3000/view/[textId]/[versionId]). At each version switch, React Router rerenders the entire page, including the TextHistory, which resets its scroll position to the top of its wrapper.

To set the position back to were it was in the previous page, I save the scroll position in Redux before leaving the said page. Then, on the new TextHistory rendering, I just read that saved position and set the scrollTop of the TextHistory component (same result with scrollTo()). Up to there, everything is perfect.

My problem is that React has the time to display the original unscrolled component BEFORE the scrollTo is taken into account, resulting in a component slightly flickering at screen. Is there a method to force that scroll to happen BEFORE React actually renders the component?

const TextHistory = props => {
  useSelector(state => state.texts.activeTextHistory.textId)
  const historyDataRef = useRef(null);

  // Component updates
  //------------------
  useEffect(() => {
    if (historyDataRef.current)
      historyDataRef.current.scrollTop = store.getState().texts.activeTextHistory.scrollPos;
  });

  // Handles
  //--------
  function handleLeave(e) {
    store.dispatch(setActiveTextHistory({ ...store.getState().texts.activeTextHistory, scrollPos: e.target.parentNode.parentNode.scrollTop }));
  }

  // Rendering
  //----------
  let versions = [];
  store.getState().texts.activeTextHistory.history.map((entry, index) => {
    let date = MySQLTimestampToDate(entry.creationDate);
    let link = `/view/${props.textId}/${entry.version}`;
    versions.push(
      <span key={index}>
        <EpistoLadsLink to={link} onLeave={handleLeave}>
          <FormattedMessage id="textViewer.version" values={{ version: entry.version }} />
        </EpistoLadsLink>
        <FormattedMessage id="textViewer.creationData" values={{ date: date.toLocaleDateString("fr-FR"), time: date.toLocaleTimeString("fr-FR") }} />
        <br />
      </span>
    );
  });
  //-----
  return (
    <div id="textHistory">
      {versions.length > 0 &&
        <Fragment>
          <FormattedMessage id="history.title"/>
          <div id="historyData" ref={historyDataRef}>{versions}</div>
        </Fragment>
      }
    </div>
  );
};

Solution

  • There is a react hooks api called useLayoutEffect which I believe can help you.

    The hook is similar to the useEffect hook but the difference is that updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

    Use it as thus -

    useLayoutEffect(() => {
        if (historyDataRef.current)
          historyDataRef.current.scrollTop = store.getState().texts.activeTextHistory.scrollPos;
      });