Search code examples
reactjsreact-forwardref

React forwardRef to access a method and not a DOM element


I'm working on a project in React 18, in TypeScript. There is a page header, inside of which is a Nav component. I'm using Bootstrap-react's NavBar component for that nav. I can do a manual toggle of the collapsible nav, and that's fine, it's all inside the same React component.

But now I need to access that toggler from the parent Header component, of which Nav is child. I'm trying to use forwardRef to accomplish this, and running into problems.

So inside of my Nav React component, I'm using Bootstrap's NavBar. There is a method I wrote to toggle the collapsible navbar, for actions that don't collapse it normally. It looks like this:

const toggleNav = () => {
  const toggle: HTMLButtonElement | null = document.querySelector(
    "button#toggle-button"
  );
  const collapseTell = document.querySelector("div#basic-navbar-nav");
  const showing = collapseTell
    ? collapseTell.className.indexOf("show") !== -1
    : false;
  if (toggle && showing) toggle.click();
};

This leverages the classNames that Bootstrap's NavBar component creates.

But now I am creating an additional element inside Header (not yet its own component, at least until I solve this issue), which, when interacted with, needs to collapse the NavBar Bootstrap component. I see a couple of ways to do this by brute force:

(1) just put this new element inside the Nav React component, where it can also access the same toggleNav method. (2) Re-create toggleNav inside of the Header React component.

I don't want to do either of these: (1) is not semantic at all...even if I don't break this new element out into its own component, it definitely doesn't belong in Nav. (2) doubles down on what feels like something kludgy to begin with, and I would like to avoid it. It appears that forwardRef is the appropriate way to do this, and I'm having some struggles with it.

Here is the parent component:

interface headerProps {
  prop1: ...
  prop2: ...
  prop3: ...
  (etc)
}

export function Header({
  prop1, prop2, prop3, ...
}: headerProps) {

  // various routines and states and things here  

  const navRef = useRef();

  // handlers and such here

  return (
    <header>
      
      // assorted JSX stuff here

      <Nav
        navProp1={}
        navProp2={}
        (etc)
        ref={navRef}
      />
    </header>
  );
}

Now here is the Nav component, in its own module:

interface headerNavProps {
  navProp1: ...
  navProp2: ...
  etc
}

export const Nav = forwardRef(
  ({navProp1, navProp2, etc}: headerNavProps, ref) => {
    
    // various states, handlers and doo-dads

    const toggleNav = () => {
      const toggle: HTMLButtonElement | null = document.querySelector(
        "button#toggle-button"
      );
      const collapseTell = document.querySelector("div#basic-navbar-nav");
      const showing = collapseTell
        ? collapseTell.className.indexOf("show") !== -1
        : false;
      if (toggle && showing) toggle.click();
    };


    return (
      <Navbar
        expand={...}
        collapseOnSelect={true}
        bg="transparent"
        variant="dark"
      >
        <Container>

          // contents of the navbar

        </Container>
      </Navbar>
    );
  }
);

I haven't even gotten to trying to reference toggleNav yet. Though VSCode does not show any compilation errors as I have it here, what I get in the browser (both the viewport and the console), is:

Component is not a function. (In 'Component(props, secondArg)', 'Component' is an instance of Object)
renderWithHooks@http://localhost:3000/static/js/bundle.js:46501:31
mountIndeterminateComponent@http://localhost:3000/static/js/bundle.js:49787:32
...(continue stack trace)

So, my questions, finally:

--What am I doing incorrectly? I THINK I've got the syntax for forwardRef correct.

--I'm not interested in referencing a DOM node with the forwardRef, but that method toggleNav. How would I do that? I THINK the way to do it is, from within the header, reference navRef.current.toggleNav, without having to attach the ref property to toggleNav...but I'm not sure.

--Do I even need to use forwardRef? As I'm looking around the Web for answers, I see that I possibly don't even need to use it, just do useRef from within the Header component, reference the Nav component with it, and then reference navRef.current.toggleNav. If I try to do that, simply that and back forwardRef out of Nav, then I get a compile error, since Nav seems to treat ref as a prop, and wants to know what gives.

Thanks for helping me untangle this!

EDIT:

I just tried it without using forwardRef at all. Just did useRef within Header, attached that ref to Nav, and within Nav added a prop ref: any to its props interface. The page renders...but on the console I get...

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Solution

  • No need for refs or even "React" methodologies: note that your toggleNav doesn't actually depend on anything in React, it's just plain browser JS. As such, you can take it out of the Nav component, put it in its own toggle-navbar.ts file, and then have anything that needs to use it, import it.

    Because remember that you're always in JavaScript: if something doesn't specifically rely on React features, you don't need to use React to make it work, and you'll end up with cleaner React code because of it.

    (And that even goes for things like effects, which by definition consist of "not React" code that has to kick in after React is done generating things. Zero reason to have a giant const block inside your component function, write a normal, proper JS function that takes the data it needs as arguments, and lives in its own module, either on its own, or with a bunch of other effects. Now your React code is super clean, with useEffect imported from react, named effects imported from your effect file(s), and clean useEffect(() => namedEffect(...), [...deps]) lines in your components)