3

In a React application, I have a grid of <Card /> components. Each <Card /> renders several <Button /> components.

Due to constraints outside my control, Button is an inefficient component. It is expensive to render, and we need to render hundreds of <Card /> components, so it becomes a performance concern.

To account for this, I only want <Button /> to mount/render when the user is actively interacting with the <Card />:

  • When the user hovers on <Card />, mount <Button />
  • When the user keyboard-navigates into <Card />, mount and focus <Button />.
  • Allow keyboard users to fully cycle forwards/backwards between <Card /> components

The hover portion is fairly straightforward, and my implementation can be seen below.

I am having trouble with the keyboard portion. Since <Button /> is not mounted yet, how can the user keyboard navigate to it?

CodeSandbox link of what I've tried

This mostly works, except in two important ways:

  1. the first <Button /> should receive focus as soon as the <Card/> is tabbed into (not requiring a second tab press)
  2. reverse keyboard navigation (ie shift+tab) does not allow any <Button> to be focused. It should focus the "last" button, and allow the user to navigate through all the buttons and then out to the previous <Card>

Question: What modifications must I make to the below code to support the above two points?

import { useState, Fragment, useEffect, useRef, MutableRefObject } from "react";
import Button from "./Button";

export default function Card() {
  const [isHovered, setIsHovered] = useState<boolean>(false);
  const [showButtons, setShowButtons] = useState<boolean>(false);
  const containerRef: MutableRefObject<HTMLDivElement | null> = useRef(null);

  useEffect(() => {
    setShowButtons(isHovered);
  }, [isHovered]);

  return (
    <div
      onMouseOver={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      onFocus={(event) => {
        event.persist();
        setIsHovered(true);
      }}
      onBlur={(event) => {
        event.persist();

        window.requestAnimationFrame(() => {
          if (containerRef.current) {
            setIsHovered(
              containerRef.current.contains(document.activeElement) === true
            );
          }
        });
      }}
      tabIndex={0}
    >
      <div ref={containerRef}>
        <h2>Card</h2>
        {showButtons && (
          <Fragment>
            <Button>Expensive Button One</Button>
            <Button>Expensive Button Two</Button>
          </Fragment>
        )}
      </div>
    </div>
  );
}
tdc
  • 5,174
  • 12
  • 53
  • 102
  • What is it that makes these buttons so computationally expensive? As rendering the buttons should take next to zero computational effort could you not render them as part of the card but use a very simple event listener on them that links to whatever is computationally expensive only on click (have a very light weight handler that then mounts the logic (which I am assuming is what is causing these buttons to be heavy) only when the button is first clicked). The current way of attempting to do this is fraught with problems as screen reader users do not navigate with Tab generally. – GrahamTheDev Nov 18 '21 at 07:55
  • For example I can navigate via a buttons list - which would be next to impossible with the dynamic mounting and unmounting, I could jump to headings which would also be really difficult (or impossible) to detect as it won't trigger a focus event etc. Having the buttons in the DOM is almost essential and any workarounds we do would be very complex to account for different navigation methods. So I am suggesting we look at moving the complexity (however we are able given the fact you said we can't change the buttons much) elsewhere and have the buttons visible. – GrahamTheDev Nov 18 '21 at 07:59
  • Could you perhaps mount all buttons that are visible on the screen (or only render the visible cards) and then lazy load the rest? I cannot imagine rendering 15-20 cards at a time could be too taxing. Then if performance suffers once a user scrolls remove the buttons when they are a certain distance off-screen again. Bear in mind you would have to ensure space is assigned for the buttons so there are no layout shifts. I hope the comments are clear, but any questions to clarify (or reasons we can't do one of the above options) let me know. **The key here is the buttons need to be rendered.** – GrahamTheDev Nov 18 '21 at 08:02
  • Using ` – QuentinC Nov 18 '21 at 12:14
  • @QuentinC this is not using the native HTML `` component. I'm using an HTML button for sake of example. This is unfortunately a hard restriction so "change the button" is not an acceptable answer. – tdc Nov 18 '21 at 16:13

0 Answers0