skip to Main Content

I have this hook that opens/closes some unique div’s. I use typescript.

import { SetStateAction, useState } from "react";

export default function useOpened() {
  const [isOpened, setIsOpened] = useState("closed");
  const open = (depName: SetStateAction<string>) =>
    setIsOpened(depName);
  return [isOpened, open];
}

This works and opens the div like so:

<button onClick={() => open(item.text)}>{item.text}</button>
{isOpened === item.text && (
<div>Text stuff</div>
)}
<button onClick={() => open(item.chapter)}>{item.chapter}</button>
{isOpened === item.chapter && (
<div>Chapter description</div>
)}

In my component, I get this problem: This expression is not callable. Not all constituents of type ‘string | ((sectionName: SetStateAction) => void)’ are callable. Type ‘string’ has no call signatures. — How to fix?

Also I try to make the hook that it toggles between "open" and "close" the div’s. I tried with setState but did not work. Also tried with status but that also did not work.

My code works but somehow when I want to open one div, then the other opened div closes. So always only one div is open always. Any other div I want to open, makes the previous one close automatically. Open/close a div should not affect any other div to open/close. Cannot figure out how to fix?

2

Answers


  1. There are three issues with your code.

    1. Both buttons set the same state

    The core issue is that those two buttons are in the same component, so they are setting the same state. If you want to two divs to open independently, you will either need to break those out into two separate components or have two separate calls to that hook.

    Something like this should work: https://playcode.io/1730761

    export function Component() {
      const [textOpened, openText] = useOpened();
      const [chapterOpened, openChapter] = useOpened();
    
      ...
    
      return (
        <>
          <button onClick={() => openText(item.text)}>{item.text}</button>
            {textOpened === item.text && (
            <div>Text stuff</div>
          )}
          <button onClick={() => openChapter(item.chapter)}>{item.chapter}</button>
            {chapterOpened === item.chapter && (
            <div>Chapter description</div>
          )}
        </>
      )
    }
    

    2. Your hook returns an Array instead of an object

    The example above only solves part of your issue, though. You are getting a TS compiler because the array is of type string | SetStateAction<string>, due to the fact that it has a mix of strings and functions. If you instead return an object from your hook, this will fix that. You can try the code below out here: https://playcode.io/1730806

    export function useOpened() {
      const [isOpened, setIsOpened] = React.useState("closed");
      const open = (depName: React.SetStateAction<string>) => setIsOpened(depName);
      return {isOpened, open}; // returning object instead of array
    }
    
    export function Component() {
      // decompose the object 
      const {isOpened: textOpened, open: openText} = useOpened();
      const {isOpened: chapterOpened, open: openChapter} = useOpened();
    
      ...
    }
    

    3. You need some way to change the state back to ‘closed’

    As coded, you only ever set the value to the name of the shown componet. You can add simple check to the hook to see if the isOpen prop is already set to the name of the component, and if so, set it to close instead:

    const open = (depName: React.SetStateAction<string>) => 
       setIsOpened(depName === isOpened ? "closed" : depName);
    

    Putting it all to gether

    The code below shows how you could pull this al together: https://playcode.io/1731497

    export function useOpened() {
      const [isOpened, setIsOpened] = React.useState("closed");
      const open = (depName: React.SetStateAction<string>) => 
        setIsOpened(depName === isOpened ? "closed" : depName);
      return {isOpened, open};
    }
    
    export function Component() {
      const {isOpened: textOpened, open: openText} = useOpened();
      const {isOpened: chapterOpened, open: openChapter} = useOpened();
    
      const item = {
        text: "Text",
        chapter: "Chapter Text",
      }
    
      return (
        <>
          <button onClick={() => openText(item.text)}>{item.text}</button>
            {textOpened === item.text && (
            <div>Text stuff</div>
          )}
          <button onClick={() => openChapter(item.chapter)}>{item.chapter}</button>
            {chapterOpened === item.chapter && (
            <div>Chapter description</div>
          )}
        </>
      )
    }
    

    After thought

    In the example below, swapped out the text prop for a boolean to toggle the open state. I also broke this up into two components. These two tweaks may end up being a little more clean and re-usable in the long run (generally breaking things up into smaller components helps in that regard), depending on your application: https://playcode.io/1731079

    export function useOpened() {
      const [isOpened, setIsOpened] = React.useState(false); // now boolean
      const toggle = () => setIsOpened(!isOpened); // now toggles
      return { isOpened, toggle };
    }
    
    // broken-out component
    export function Openable({buttonText, textToDisplay}) {
      const { isOpened, toggle } = useOpened();
    
      return (
        <>
          <button onClick={() => toggle()}>{buttonText}</button>
          {isOpened && (
            <div>{textToDisplay}</div>
          )}
        </>
      )
    }
    
    // simplified original component
    export function Component() {
      const item = {
        text: "Text",
        chapter: "Chapter Text",
      }
      return (
        <>
          <Openable buttonText={item.text} textToDisplay="Text stuff" />
          <Openable buttonText={item.chapter} textToDisplay="Chapter description" />
        </>
      )
    }
    
    Login or Signup to reply.
  2. When you want to toggle an item you need to move between 2 states. It might be false and true or key exists / absent.

    Toggling multiple items requires multiples states, 1 for each item, so that one item’s toggle state won’t affect the rest.

    To support a dynamic number of items, without using useState for each, your state can be an object, and you can change the state by adding/removing a key, where each key controls one item.

    const { useState, useCallback } = React;
    
    const useMultiToggle = (initialOpen = []) => {
      const [isOpen, setIsOpen] = useState(
        () => Object.fromEntries(initialOpen.map(key => [key, true]))
      );
      
      const toggleIsOpen = useCallback(key => {
        // destructure the item out to check if it exists
        setIsOpen(({ [key]: open, ...rest }) => open // true or undefined
          ? rest // if exists remove from state
          : { ...rest, [key]: true } // add to state
        );
      }, []);
      
      return [isOpen, toggleIsOpen];
    };
    
    const items = ['Toggle 1', 'Toggle 2', 'Toggle 3', 'Toggle 4'];
    
    const Demo = () => {
      const [isOpen, toggleIsOpen] = useMultiToggle(["Toggle 2", "Toggle 3"]);
    
      return (
        <div>
        {items.map(t => (
          <div key={t}>
            <button onClick={() => toggleIsOpen(t)}>{t}</button>
            {isOpen[t] && `${t} is open`}
          </div>
        ))}
        </div>
      );
    }
    
    ReactDOM
      .createRoot(root)
      .render(<Demo />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    Regarding Typescript. The custom hook returns a tuple of 2 – the object, and a toggle function. Because tuple in TS is just an array, you need to type it explicitly by stating the return type of the hook (sandbox):

    type UseMultiToggleReturn = [Record<string, boolean>, (key: string) => void];
    
    const useMultiToggle = (initialOpen: string[] = []): UseMultiToggleReturn => {
      const [isOpen, setIsOpen] = useState(() =>
        Object.fromEntries(initialOpen.map((key) => [key, true]))
      );
    
      const toggleIsOpen = useCallback((key: string) => {
        setIsOpen(({ [key]: open, ...rest }) =>
          open ? rest : { ...rest, [key]: true }
        );
      }, []);
    
      return [isOpen, toggleIsOpen];
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search