Home  >  Q&A  >  body text

How can I set a button to switch all accordions on and off?

<p>I have a component that creates a collapsible accordion using the following code: </p> <pre class="brush:php;toolbar:false;">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;</pre> <p>I use it in several places in the main application and sometimes in other accordions. In multiple instances, I actually had no idea how many accordions there would be on the page because they were rendered dynamically based on the data. With this in mind, I want to create a button in the main application that turns on (and off) all accordions without having to set a fixed number, and without having to render all accordions in the main application (i.e. some accordions in other component and then imported into the main application). </p> <p>I tried using ref hooks to achieve this: </p> <ol> <li>Add ref in the button of the Collapsible component and access it from the parent component through props: </li> </ol> <pre class="brush:php;toolbar:false;"><button className={props.level} onClick={toggle} ref={props.innerRef}>{props.label}</button> ;</pre> <ol start="2"> <li>Add the following ref in the main application: </li> </ol> <pre class="brush:php;toolbar:false;">const childRef = useRef(); const openClick = () => { childRef.state = true; } const closeClick = () => { childRef.state = false; }</pre> <ol start="3"> <li>Use the following buttons in the main application: </li> </ol> <pre class="brush:php;toolbar:false;"><button onClick = {openClick}> Expand All </button> <button onClick = {closeClick}> Collapse all </button></pre> <ol start="4"> <li>Add ref to accordion when rendering: </li> </ol> <pre class="brush:php;toolbar:false;"><Collapsible label="" level="content" innerRef={childRef}></pre> <p>This actually does nothing, probably because the way I was trying to access the state in step 2 was wrong, but I figured it was worth a try...</p> <p>Any ideas on if this is possible? </p>
P粉391677921P粉391677921436 days ago639

reply all(2)I'll reply

  • P粉441076405

    P粉4410764052023-08-31 15:23:23

    You can use Redux.

    1. When you render accordions, give them a specific id and save it in storage.
    2. Create a sliceopenAllAccordions, loop through the IDs, and set the accordions belonging to that ID to open=true
    3. Create a slicecloseAllAccordions, loop through the IDs, and set the accordions belonging to that ID to open=false

    reply
    0
  • P粉809110129

    P粉8091101292023-08-31 12:37:09

    In a more or less arbitrary collection of component instances, it is common for some coordination to be required. One approach I've used successfully is to create a Context with an associated hook that the component can register with. This hook returns a view of the shared state and a function that modifies that state to suit your needs.

    Here you can create a opener function that stores each registered component and provide a Context for the openAll/closeAll functions:

    const AccordionProvider = ({ children }) => {
      const [openers] = useState(new Set());
    
      // 当创建时,是否应该展开新的可折叠项?
      //(支持递归展开是必要的)
      const [defaultOpen, setDefaultOpen] = useState(false);
    
      const register = useCallback(
        (opener) => {
          openers.add(opener);
          return () => openers.delete(opener); // 返回一个用于`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}
        />
      );
    };
    

    ...There is also a hook called by each child component to register with the context and return the familiar toggle/open value:

    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 };
    };
    

    There is also a separate hook for performing batch operations which is also convenient:

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

    Sandbox

    Please note that for simplicity, a separate opener (aka setOpen) function is used here as a unique identifier for each registered component. A flexible alternative is to use other identifiers, so you can open/close arbitrary accordions during navigation etc.

    reply
    0
  • Cancelreply