Search code examples
javascriptreactjsref

How to control sibling elements' behaviour with refs?


I have Modal component with info boxes. In each InfoBox a button (icon) reveals more info about a certain ingredient of the product. That functionality is contained within each InfoBox component.

When you click the button (icon) in one InfoBox, I want all other InfoBox(es) to close - how can I control sibling elements' behaviour using refs?

I'm trying to:

  1. pass a function down to each InfoBox
  2. when one box is clicked send that event up to parent element (Modal)
  3. then in the parent element, loop through the array of refs, find the one in which the icon was clicked and through useImperativeHandle call the closeShowMore function in all other refs.

But I don't know how to mark that particular ref in the refarray where the button has been clicked.

Thanks in advance for your time!

PARENT:

const Modal = forwardRef(({ beer }, ref) => {
  // stuff removed for brev
  
  const [infoRef, setInfoRef] = useState([]);

  const closeInfoBox = (valuePassedfromChildsEvent) => {
    // here I would loop through infoRef, find the ref in which the button was clicked and then close the rest through useImperativeHandle()
  };

  useEffect(() => {
    // create an array of refs for infoBoxes
    setInfoRef(
      Array(6)
        .fill()
        .map((_, i) => infoRef[i] || createRef())
    );
  }, []);

  if (display) {
    return (
      <div className="modal-wrapper">
        <div ref={backgroundRef} className="modal-background"></div>
        <div className="modal-box">
          <div className="info-box-wrapper">
            <InfoBox
              ref={infoRef[0]}
              name="abv"
              value={abv}
              imageUrl={"../../assets/abv-white-2.svg"}
              showMoreActive={closeInfoBox}
            />
            <InfoBox
              ref={infoRef[1]}
              name="ibu"
              value={ibu}
              imageUrl={"../../assets/ibu-white.svg"}
              showMoreActive={closeInfoBox}
            />
            <InfoBox
              ref={infoRef[2]}
              name="fg"
              value={target_fg}
              imageUrl={"../../assets/style-white.svg"}
              showMoreActive={closeInfoBox}
            />
           // a few more of these 
          </div>
        </div>
      </div>
    );
  }
  return null;
});

export default Modal;

CHILD:

const InfoBox = forwardRef((props, ref) => {
  const [showMore, setShowMore] = useState(false);
  const { name, value, imageUrl, showMoreActive } = props;

  useImperativeHandle(ref, () => ({
    openShowMore,
    closeShowMore,
    // is there a way to add an 'isActive' state/var here so I can access this from the loop in the parent and say if(!infoRef.current.isActive) {closeShowMore()}?
  }));

  const openShowMore = () => {
    setShowMore(true);
    showMoreActive(showMore) // I tried sending up this state value to parent to mark the active child but this always logs false when passed to parent, even when true. why?
  };

  const closeShowMore = () => {
    setShowMore(false);
  };

  return (
    <div className={`${name}`}>
      <div className="info-box-container">
        <div className="info-box-container-top">
          <img src={imageUrl} alt="icon" />
          <div className="info-wrapper">
            <h4>{name}</h4>
            <h5>{value}</h5>
          </div>
          <div
            className="plus-minus-icon-wrapper"
            onClick={() => openShowMore()}
          >
            {!showMore ? <GoPlus /> : <FiMinus />}
          </div>
        </div>
        {showMore && (
          <div className="info-box-conetiner-bottom">
            <p>
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia
              optio ab ullam! Porro molestias accusantium et laborum distinctio
            </p>
          </div>
        )}
      </div>
    </div>
  );
});

export default InfoBox;

Solution

  • You are making it unnecessarily complicated. What you want is to use as little state as possible, and to keep that state in the smallest common parent for all affected components.

    In your case, what you want to do is to have a state variabel in Modal that goes something like this:

    const [expandedInfoBoxName, setExpandedInfoBoxName] = useState(null);
    

    You pass these props down to your InfoBox:

    <InfoBox
        name="ibu"
        {your other props}
        expandInfoBox={() => setExpandedInfoBoxName("ibu")
        showMore={expandedInfoBoxName === "ibu"}
    />
    

    In your InfoBox you remove your state and the functions useImperativeHandle, openShowMore and closeShowMore. Add this to get your new props:

    const { name, value, imageUrl, expandInfoBox, showMore } = props;
    

    Then in your jsx you do:

    <div
       className="plus-minus-icon-wrapper"
       onClick={expandInfoBox}
    >
    

    That's it. In short, you do not want to spread out your state. Always ask yourself what the minimum state you need to describe something is.