skip to Main Content

I have a component that creates a collapsible accordion, using the following code:

import React, {useState} from 'react';

const Collapsible = (props) =>{
    const [open, setOpen] = useState(false);
    const toggle = () => {
        setOpen(!open);
    }
    return(
        <div>
            <button className={props.level} onClick={toggle}>{props.label}</button>
            {open && (
                <div className="toggle">
                    {props.children}
                </div>
            )}
        </div>
    )
}

export default Collapsible;

I use this in multiple places in my main app, sometimes using accordions within other accordions. In multiple instances, I don’t actually know how many accordions will be on the page, because they are dynamically rendered based on the data. With this in mind, I want to create a button in the main app that would open (and another that would close) all accordions, without having a set number in mind, and without rendering them all in the main app (i.e. some accordions are rendered in other components, that are then imported into the main app).

I’ve tried using ref hooks to accomplish this:

  1. Added ref in the Collapsible component’s button, accessed from parent through props:
 <button className={props.level} onClick={toggle} ref={props.innerRef}>{props.label}</button>
  1. Adding the following ref in the main app:
const childRef = useRef();
const openClick = () => {
   childRef.state = true;
}

const closeClick = () => {
   childRef.state = false;
}
  1. Using the following buttons in the main app:
<button onClick = {openClick}>
   Expand all
</button>
<button onClick = {closeClick}>
   Collapse all
</button>
  1. Adding the ref to the accordion when rendering:
<Collapsible label="" level="content" innerRef={childRef}>

This does absolutely nothing, likely because you can’t access state in the way I’m attempting in #2, but I thought it was worth a shot…

Any thoughts on whether this is doable?

2

Answers


  1. You can use Redux.

    1. When you render you accordions give them a specific id and save it in the store.
    2. Create a slice openAllAccordions that loop throw IDs and set the accordion belongs to that id to open=true
    3. Create a slice closeAllAccordions that loop throw IDs and set the accordion belongs to that id to open=false
    Login or Signup to reply.
  2. It’s pretty common to have a more-or-less arbitrary collection of component instances that need some coordination. An approach that I have had success with is to create a Context with an associated hook that components can use to register. The hook returns a view of the shared state and functions to modify that state, subject to your needs.

    Here, you could create a Context that stores the opener functions for each of the registered components, and provides openAll/closeAll functions:

    const AccordionProvider = ({ children }) => {
      const [openers] = useState(new Set());
    
      // Should new collapsibles spring open when created?
      // (necessary to support recursive opening)
      const [defaultOpen, setDefaultOpen] = useState(false);
    
      const register = useCallback(
        (opener) => {
          openers.add(opener);
          return () => openers.delete(opener); // return a deregister function for `useEffect`
        },
        [openers]
      );
    
      const openAll  = useCallback(() => {
        setDefaultOpen(true);
        openers.forEach(opener => opener(true)),
      }, [setDefaultOpen, openers]);
    
      const closeAll = useCallback(() => {
        setDefaultOpen(false);
        openers.forEach(opener => opener(false)),
      }, [setDefaultOpen, openers]);
    
      return (
        <AccordionContext.Provider
          value={{ register, openAll, closeAll, defaultOpen }}
          children={children}
        />
      );
    };
    

    … and a hook called by each child that registers with the context, and returns your familiar toggle/open values:

    const useAccordionAsClient = () => {
      const { register, defaultOpen } = useContext(AccordionContext);
    
      const [open, opener] = useState(defaultOpen);
      const toggle = useCallback(() => opener((open) => !open), [opener]);
    
      useEffect(() => register(opener), [register, opener]);
    
      return { toggle, open };
    };
    

    It’s also handy to have a separate hook for the actions that you can execute en masse:

    const useAccordionAsManager = () => {
      const { openAll, closeAll } = useContext(AccordionContext);
    
      return { openAll, closeAll };
    };
    

    Sandbox

    Note that for simplicity this is just using the individual opener (aka setOpen) functions as unique identifiers for each registered component. A flexible alternative would be use some other identifier, so you could open/close arbitrary accordions on navigation etc.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search