简体   繁体   中英

React Stale State with Custom Hook Event Handling

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.

CodeSandbox

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM