It seems I just can't get my head around stale state issues in React as it relates to event handlers and hooks. I conceptually understand what is happening–there is a closure that is capturing the starting value of a state value, and isn't updating when I expect it to.
I am creating a NavBar component onto which I want to add keyboard controls to allow accessibility for sub menus and so forth. I will show the code and then describe what is happening / not happening. I'll also link to a codesandbox for easier forking and debugging.
NavBar
const NavBar: React.FC<Props> = ({ children, label }) => {
const {
actions: { createNavItemRef },
state: { activeSubMenuIndex, navBarRef },
} = useNav();
console.log('NAV.BAR', { activeSubMenuIndex });
return (
<nav aria-label={label} ref={navBarRef}>
<NavList aria-label={label} role="menubar">
{children} // NavItems
</NavList>
</nav>
);
};
NavItem
const NavItem: React.FC<Props> = ({ children, hasSubMenu, to }) => {
const ChildrenArray = React.Children.toArray(children);
const {
actions: { handleSelectSubMenu },
state: { activeSubMenuIndex },
} = useNav();
const handleSubMenuToggle = () => {
handleSelectSubMenu(index);
};
return (
<li ref={ref} role="none">
<>
<ParentButton
aria-expanded={activeSubMenuIndex === index}
aria-haspopup="true"
onClick={handleSubMenuToggle}
role="menuitem"
tabIndex={index === 0 ? 0 : -1}
>
{ChildrenArray.shift()}
</ParentButton>
{ChildrenArray.pop()}
</>
</li>
);
};
UseNav
function useNav() {
const navBarRef = useRef<HTMLUListElement>(null);
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number | undefined>();
const handleSelectSubMenu = (index?: number) => {
if (!index || activeSubMenuIndex === index) {
setActiveSubMenuIndex(undefined);
} else {
setActiveSubMenuIndex(index);
}
};
useEffect(() => {
const navbar = navBarRef?.current;
navbar?.addEventListener('keydown', () => {
console.log("UseNav", { activeSubMenuIndex });
});
// return () => remove event listener
}, [navBarRef, activeSubMenuIndex]);
return {
actions: {
createNavItemRef,
handleSelectSubMenu,
},
state: {
activeSubMenuIndex,
navBarRef,
},
};
}
This is a somewhat stripped down version of my set up. Ultimately, here's what's going on.
Expectation I tab onto the first NavItem
and it becomes focused. I hit an arrow key (for example) and the log UseNav { activeSubMenuIndex })
logs out correctly as undefined
.
Then I click on the NavItem which contains a sub menu. The activeSubMenuIndex
updates and in the NavItem
the correct sub menu is displayed (based on the activeSubMenuIndex === index
conditional).
However, I would expect the NavBar { activeSubMenuIndex })
to log out as well when this NavItem is clicked. But it doesn't.
With the sub menu visible, I hit another arrow key and when the UseNav
log is displayed, I would expect it to contain the correct activeSubMenuIndex
value, but it is still undefined
.
Ultimately, I will need to addEventListeners for keyPress
on the NavBar in order to assign keyboard navigation throughout. But if I can't even get the state values updating correctly at this MVP level, then I can't really move forward without making this more cumbersome to work with and debug later.
I know this is an issue of stale state, but I can't find any good articles on this topic that isn't just incrementing a number within the same file. So any help in finally cracking through this wall would be amazing.
Thank you!
Looks like this issue stemmed from the use of the useNav()
hook. When I call this hook inside of NavBar
references and values are instantiated once. When I call the hook again in NavItem
those same refs and values are instantiated again.
In this case, instead of a hook, it would make more sense to wrap this in a context in order to keep the logic out from the UI but keep the components consistent in their data sources.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.