Toni Michel Caubet
Toni Michel Caubet

Reputation: 20173

Scroll to anchor when Component is fully rendered in React functional component

This a (very) simplified version of my component:

export const DynamicComponent: FC<DynamicComponentProps> = (props) => {
  const ref = useRef<HTMLElement>(null);
  const [isSticked, setIsSticked] = useState(false);
  const parentSticked = useContext(StickyContext);
  const [overridedStyles, setOverridedStyles] = useState(props.styles ?? {});
  const [overridedArgs, setOverridedArgs] = useState(props.args ?? {});
  const { config } = useContext(GlobalContext);
  const data = useContext(DataContext);
  const [state, setState] = useContext(StateContext);

  const mountComponent = useMemo(() => {
    if (typeof props.mount === "undefined") return true;
    if (typeof props.mount === "boolean") return props.mount;
    if (typeof props.mount === "number") return props.mount === 1;
    if (typeof props.mount === "string") {
      let mount = stateParser.parse(props.mount, state) as unknown;
      return mount == true;
    }
    return false;
  }, [state, props.mount]);



  useLayoutEffect(() => {
    setTimeout(() => {
      const anchorHash = location.hash;
      if (
        anchorHash &&
        document &&
        document.querySelector(anchorHash) &&
        !document
          .querySelector(anchorHash)
          ?.classList.contains("already-scrolled")
      ) {
        document?.querySelector(anchorHash)?.scrollIntoView();
        document?.querySelector(anchorHash)?.classList.add("already-scrolled");
      }
    }, 50);
  }, []);

  let output = mountComponent ? (
    <StickyContext.Provider value={{ sticked: isSticked }}>
      <StyledDynamicComponent
        {...props}
        ref={ref}
        isSticked={applyStickedStyles}
        args={overridedArgs}
        styles={overridedStyles}
      />
    </StickyContext.Provider>
  ) : null;

  return output;
};

The code inside the useLayoutEffect won't run correctly without the setTimeout because the component is not fully rendered and document?.querySelector(anchorHash) does not exist yet..

Tried with a window.onload but the code inside it will never run..

Is there a way to prevent using that horrendous setTimeout?

Also please note that the anchor or the anchored element are optional so I don't know how to use callaback refs

Upvotes: 2

Views: 3328

Answers (1)

Felix
Felix

Reputation: 2691

Don't use document.querySelector and don't check class names, if you can use states for it.

You don't need setTimeout at all, as useEffect and useEffectLayout are more or less the same as componentDidMount:

If you’re migrating code from a class component, note useLayoutEffect fires in the same phase as componentDidMount and componentDidUpdate. However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem. useLayoutEffect-Docs

I tried to reduce your samle a little bit more and made it debuggable in the codesandbox (hopefully keeping your logic in tact).

Edit React PlayGround (forked)

But the most important part would be the following:

const ref = useRef();

useEffect(() => {
    if (!ref.current || !document) {
      return;
    }

    // check if a hash is provided
    // possible todo: is the current element id the same as the provided location hash id
    if(!location.hash) {
      return true;
    }

    // check if we've scrolled already
    if(scrolled) {
      return;
    }

    ref.current.scrollIntoView();

    console.log("scroll to view", ref);
    setScrolled(true);
}, [ref, location, scrolled]);


Your component will then be rendered each time, the ref, location, or scrolled vars have changed, but it should only scroll into view, if it hasn't done that before.

Upvotes: 4

Related Questions