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()?
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)