Search code examples
reactjsdomreact-router-domselectors-api

document.querySelector() always return null when clicking on React Router Link the first time but will return correctly after


I'm stucking on a problem with React-Router and querySelector.

I have a Navbar component which contains all the CustomLink components for navigation and a line animation which listens to those components and displays animation according to the current active component.

// Navbar.tsx

import React, { useCallback, useEffect, useState, useRef } from "react";
import { Link, useLocation } from "react-router-dom";

import CustomLink from "./Link";

const Layout: React.FC = ({ children }) => {
  const location = useLocation();
  const navbarRef = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState({ left: 0, width: 0 });

  const handleActiveLine = useCallback((): void => {
    if (navbarRef && navbarRef.current) {
      const activeNavbarLink = navbarRef.current.querySelector<HTMLElement>(
        ".tdp-navbar__item.active"
      );

      console.log(activeNavbarLink);

      if (activeNavbarLink) {
        setPos({
          left: activeNavbarLink.offsetLeft,
          width: activeNavbarLink.offsetWidth,
        });
      }
    }
  }, []);

  useEffect(() => {
    handleActiveLine();
  }, [location]);

  return (
    <>
      <div className="tdp-navbar-content shadow">
        <div ref={navbarRef} className="tdp-navbar">
          <div className="tdp-navbar__left">
            <p>Todo+</p>
            <CustomLink to="/">About</CustomLink>
            <CustomLink to="/login">Login</CustomLink>
          </div>
          <div className="tdp-navbar__right">
            <button className="tdp-button tdp-button--primary tdp-button--border">
              <div className="tdp-button__content">
                <Link to="/register">Register</Link>
              </div>
            </button>
            <button className="tdp-button tdp-button--primary tdp-button--default">
              <div className="tdp-button__content">
                <Link to="/login">Login</Link>
              </div>
            </button>
          </div>
          <div
            className="tdp-navbar__line"
            style={{ left: pos.left, width: pos.width }}
          />
        </div>
      </div>
      <main className="page">{children}</main>
    </>
  );
};

export default Layout;
// CustomLink.tsx
import React, { useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";

interface Props {
  to: string;
}

const CustomLink: React.FC<Props> = ({ to, children }) => {
  const location = useLocation();
  const history = useHistory();
  const [active, setActive] = useState(false);

  useEffect(() => {
    if (location.pathname === to) {
      setActive(true);
    } else {
      setActive(false);
    }
  }, [location, to]);

  return (
    // eslint-disable-next-line react/button-has-type
    <button
      className={`tdp-navbar__item ${active ? "active" : ""}`}
      onClick={(): void => {
        history.push(to);
      }}
    >
      {children}
    </button>
  );
};

export default CustomLink;

But it doesn't work as I want. So I opened Chrome Devtool and debugged, I realized that when I clicked on a CustomLink first, the querySelector() from Navbar would return null. But if I clicked on the same CustomLink multiple times, it would return properly, like the screenshot below:

Error from Chrome Console

How can I get the correct return from querySelector() from the first time? Thank you!


Solution

  • It's because handleActiveLine will trigger before setActive(true) of CustomLink.tsx

    Add a callback in CustomLink.tsx:

    const CustomLink: React.FC<Props> = ({ onActive }) => {
    
      useEffect(() => {
        if (active) {
          onActive();
        }
      }, [active]);
    
    }
    

    In Navbar.tsx:

    const Layout: React.FC = ({ children }) => {
      
      function handleOnActive() {
         // do your query selector here
      }
    
      // add onActive={handleOnActive} to each CustomLink
      return <CustomLink onActive={handleOnActive} />
    }