Create Collapsible components using react hooks
Tags: beginners, react, javascript, typescript
Published: 2022-12-24 19:04:16

[React collapsed] (https://github.com/roginfarrer/react-collapsed) is a handy library for creating Collapsible components. It is a headless library, meaning rather than exposing fully styled or minimally styled components it gives us a hook which we can use to create our own Collapsible components. The benefit of such libraries is that you don't have to fight with the internal styles of the component. Styling the components is on the consumer of the library while the logic and state is managed by the hook. Reading through the react-collapsed library code, in this tutorial we will - 
- First create a (rigid) Collapsible component.
- Refactor our component into a (generic) hook.

Please check the working demo [here](http://yaldram-react-collapsed.s3-website.ap-south-1.amazonaws.com/).

### Step One - Project setup
Create a react typescript project, from your terminal run and set the project up by completing the simple questionnaire - 
```bash
npm create vite@latest react-collapse
```
Get rid of all the default boilerplate code. Create 2 new files namely `Collapse.tsx` and `utils.ts` under `utils.ts` paste the following - 
```typescript
export const noop = () => {}
```

### Step Two - Styles and Props setup
Our `Collapsible` component will have 2 main elements namely `Toggle` & `Collapsible`. A toggle can be a button, div, on clicking the toggle we will expand / collapse the Collapsible body. Under `Collapse.tsx` paste the following - 
```typescript
import * as React from 'react';

const collapseHeaderStyles: React.CSSProperties = {
  boxSizing: 'border-box',
  border: '2px solid black',
  color: '#212121',
  fontFamily: 'Helvetica',
  padding: '12px',
  fontSize: '16px',
  cursor: 'pointer',
};

const collapseBodyStyles: React.CSSProperties = {
  boxSizing: 'border-box',
  border: '2px solid black',
  borderTop: 'none',
  color: '#212121',
  fontFamily: 'Helvetica',
  padding: '12px',
  fontSize: '16px',
};

export function Collapse() {
  return (
    <div>
      <div style={collapseHeaderStyles}>Toggle</div>
      <div
        style={{
          boxSizing: 'border-box',
        }}
      >
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptate
          dicta, sapiente, praesentium vero sed deleniti ea dolorum hic commodi
          dolores quam itaque unde, architecto alias.
        </div>
      </div>
    </div>
  );
}
```
In our App.tsx lets use our collapse component - 
```typescript
import { Collapse } from "./Collapse";

export function App() {
  return <Collapse />;
}
```
Run the project you should be able to see this UI 
![collapse-one](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tnxm6mg4p3301ylsplx.png)

### Step Three - Component Props
```typescript
type CollapsibleProps = {
  /** default state of the collapsible */
  isOpened?: boolean;
  /** triggered when the component starts closing  */
  onCollapseStart?: () => void;
  /** triggered after the component is closed */
  onCollapseEnd?: () => void;
  /** triggered when the component starts opening  */
  onExpandStart?: () => void;
  /** triggered after the component is opened */
  onExpandEnd?: () => void;
  /** triggered on open and close */
  onToggle?: () => void;
};

export function Collapse({
  isOpened = false,
  onCollapseStart = noop,
  onCollapseEnd = noop,
  onExpandStart = noop,
  onExpandEnd = noop,
  onToggle = noop,
}: CollapsibleProps) {}
```
- `isOpened` is the default state of the component whether it should be opened or closed when the page first loads.
-  `onExpandStart` is triggered when the expand animation starts, component is opening.
- `onExpandEnd` is triggered when the expand animation ends, component has opened.
- `onCollapseStart` is triggered when the collapse animation starts, component is closing.
- `onCollapseEnd` is triggered when the collapse animation ends, component has closed.
- `onToggle` is triggered after the component opens and closes, (we will remove it later during the hook refactor).

### Step Four - Component State
```typescript
const easing = "cubic-bezier(0.4, 0, 0.2, 1)";

const duration = 266;

const collapsedStyles = {
  display: "none",
  height: "0px",
  overflow: "hidden",
};

export function Collapse({
  isOpened = false,
  onCollapseStart = noop,
  onCollapseEnd = noop,
  onExpandStart = noop,
  onExpandEnd = noop,
  onToggle = noop,
}: CollapsibleProps) {
  const [isExpanded, setExpanded] = React.useState(isOpened);

 const [collapsibleStyles, setStylesRaw] = React.useState<React.CSSProperties>(
    isOpened ? {} : collapsedStyles
  );

  const uniqueId = React.useId();

  const containerRef = React.useRef<HTMLDivElement | null>(null);

  return (
    <div>
      <div
        id={`react-collapsed-toggle-${uniqueId}`}
        aria-controls={`react-collapsed-panel-${uniqueId}`}
        aria-expanded={isExpanded}
        tabIndex={0}
        style={collapseHeaderStyles}
        onClick={() => {
          onToggle();
        }}
      >
        {isExpanded ? "Close" : "Open"}
      </div>
      <div
        ref={containerRef}
        style={{
          boxSizing: "border-box",
          ...collapsibleStyles,
        }}
      >
        <div
          id={`react-collapsed-panel-${uniqueId}`}
          aria-hidden={!isExpanded}
          style={collapseBodyStyles}
        >
          Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptate
          dicta, sapiente, praesentium vero sed deleniti ea dolorum hic commodi
          dolores quam itaque unde, architecto alias.
        </div>
      </div>
    </div>
  );
}
```
- `isOpened` is the state of the `Collapsible` opened or closed.
- `collapsibleStyles` will keep a track of the styles of the `Collapsible body` height, transition, etc. we are basically changing the styles when we transition from expand to collapse or vice-versa. 
- Say if we are in closed (isExpanded = false) state the height of the `Collapsible body` is 0px and when we transition to opened (isExpanded = true) state the height will be the height of the container.
- Also, we are using a ref, that we have attached to the `Collapsible body` element, to get its height.  Now under `utils.ts` paste - 
```typescript
export function getElementHeight(
  el: RefObject<HTMLElement> | { current?: { scrollHeight: number } }
): string | number {
  if (!el?.current) {
    return "auto";
  }

  return el.current.scrollHeight;
}

type AnyFunction = (...args: any[]) => unknown;

export const callAll =
  (...fns: AnyFunction[]) =>
  (...args: any[]): void =>
    fns.forEach((fn) => fn && fn(...args));
```
Run the project from the terminal and check the output - 
![collapse-two](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tswf69y7andhkvqnl229.png)

Now pass `isOpened` prop as true to your Collapse component under `App.tsx` and check the output - 
```typescript
export function App() {
  return <Collapse isOpened />;
}
```
![collapse-three](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17ue7yp0fbidmq4xn38e.png)

### Step Four - Logic
Now our next task is, when we click on the `Toggle Element` `Collapsible Header` in our case, the `Collapsible Body` should open and if it is opened it should close. In `Collapse.tsx` paste -
```typescript
import * as React from "react";
import { flushSync } from "react-dom";

import { callAll, getElementHeight, noop } from "./utils";

const collapseHeaderStyles: React.CSSProperties = {
  boxSizing: "border-box",
  border: "2px solid black",
  color: "#212121",
  fontFamily: "Helvetica",
  padding: "12px",
  fontSize: "16px",
  cursor: "pointer",
};

const collapseBodyStyles: React.CSSProperties = {
  boxSizing: "border-box",
  border: "2px solid black",
  borderTop: "none",
  color: "#212121",
  fontFamily: "Helvetica",
  padding: "12px",
  fontSize: "16px",
};

type CollapsibleProps = {
  /** default state of the collapsible */
  isOpened?: boolean;
  /** triggered when the component starts closing  */
  onCollapseStart?: () => void;
  /** triggered after the component is closed */
  onCollapseEnd?: () => void;
  /** triggered when the component starts opening  */
  onExpandStart?: () => void;
  /** triggered after the component is opened */
  onExpandEnd?: () => void;
  /** triggered on open and close */
  onToggle?: () => void;
};

const easing = "cubic-bezier(0.4, 0, 0.2, 1)";

const duration = 266;

const collapsedStyles = {
  display: "none",
  height: "0px",
  overflow: "hidden",
};

export function Collapse({
  isOpened = false,
  onCollapseStart = noop,
  onCollapseEnd = noop,
  onExpandStart = noop,
  onExpandEnd = noop,
  onToggle = noop,
}: CollapsibleProps) {
  const [isExpanded, setExpanded] = React.useState(isOpened);

  const [collapsibleStyles, setStylesRaw] = React.useState<React.CSSProperties>(
    isOpened ? {} : collapsedStyles
  );

  const uniqueId = React.useId();

  const containerRef = React.useRef<HTMLDivElement | null>(null);

  const setStyles = (newStyles: {} | ((oldStyles: {}) => {})) => {
    flushSync(() => {
      setStylesRaw(newStyles);
    });
  };

  const mergeStyles = (newStyles: {}) => {
    setStyles((oldStyles) => ({ ...oldStyles, ...newStyles }));
  };

  function handleTransitionEnd(e: React.TransitionEvent) {
    if (e.target !== containerRef.current || e.propertyName !== "height") {
      return;
    }

    if (isExpanded) {
      const height = getElementHeight(containerRef);

      // If the height at the end of the transition
      // matches the height we're animating to,
      if (height === collapsedStyles.height) {
        setStyles({});
      } else {
        // If the heights don't match, this could be due the height
        // of the content changing mid-transition
        mergeStyles({ height });
      }

      onExpandEnd();
    }

    onCollapseEnd();
  }

  function expandCollapse(nextState: boolean) {
    setExpanded(nextState);
    if (nextState) {
      requestAnimationFrame(() => {
        onExpandStart();
        mergeStyles({
          willChange: "height",
          display: "block",
          overflow: "hidden",
        });

        requestAnimationFrame(() => {
          const height = getElementHeight(containerRef);
          mergeStyles({
            transition: `height ${duration}ms ${easing}`,
            height,
          });
        });
      });
    } else {
      requestAnimationFrame(() => {
        onCollapseStart();
        const height = getElementHeight(containerRef);
        mergeStyles({
          transition: `height ${duration}ms ${easing}`,
          willChange: "height",
          height,
        });

        requestAnimationFrame(() => {
          mergeStyles({
            height: "0px",
            overflow: "hidden",
          });
        });
      });
    }
  }

  return (
    <div>
      <div
        id={`react-collapsed-toggle-${uniqueId}`}
        aria-controls={`react-collapsed-panel-${uniqueId}`}
        aria-expanded={isExpanded}
        tabIndex={0}
        style={collapseHeaderStyles}
        onTransitionEnd={handleTransitionEnd}
        onClick={callAll(() => expandCollapse(!isExpanded), onToggle)}
      >
        {isExpanded ? "Close" : "Open"}
      </div>
      <div
        ref={containerRef}
        style={{
          boxSizing: "border-box",
          ...collapsibleStyles,
        }}
      >
        <div
          id={`react-collapsed-panel-${uniqueId}`}
          aria-hidden={!isExpanded}
          style={collapseBodyStyles}
        >
          Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptate
          dicta, sapiente, praesentium vero sed deleniti ea dolorum hic commodi
          dolores quam itaque unde, architecto alias.
        </div>
      </div>
    </div>
  );
}
``` 
- On clicking the collapse header, we will call the `expandCollpase` function and pass in the nextState. 
- If we are opening the component, we transition from height 0px to the height of the element and vice-versa.
- We use `requestAnimationFrame` for performing fast and performant animations. For more info please check [this video](https://www.youtube.com/watch?v=vBvV2c-x1MA).
- To know more about flushSync you can check the docs [here](https://beta.reactjs.org/apis/react-dom/flushSync). Automatic batching in React is indeed an amazing feature. But, there can be situations where we need to prevent this from happening. For that, React provides a method named flushSync() in react-dom that allows us to trigger a re-render for a specific state update.
- We want to read height of the `Collapsible body` so we make sure we do all the changes to the DOM before we read the height. 
- Now from the terminal run npm run dev and check the collapsible component.

### Step Five: Sync with external store
Many a times, you would like to control the `Collapsible component` from an outside piece of state. For that to work we will use -  
 - `useEffect` in our Collapse component to sync external state with our Collapse component state. This `useEffect` should only run after our component has mounted.
 - Pass onToggle prop to our collapse component so that we can sync our internal state with the outside component.
Paste the following in the `Collapsible.tsx` - 
```typescript
 const mounted = React.useRef<boolean>(false);

 React.useEffect(() => {
   if (mounted.current) {
     expandCollapse(isOpened);
   }
   mounted.current = true;
 }, [isOpened]);
```

Paste the following in `App.tsx` and check our Collapsible - 
```typescript
import * as React from 'react';
import { Collapse } from './Collapse';

export function App() {
  const [isExpanded, setIsExpanded] = React.useState(false);

  function toggleCollapse() {
    setIsExpanded((prevState) => !prevState);
  }

  return (
    <React.Fragment>
      <button onClick={toggleCollapse}>Toggle</button>
      <Collapse isOpened={isExpanded} onToggle={toggleCollapse} />
    </React.Fragment>
  );
}
```

### Step Six: Turn component into a generic hook
From the component code we can see we have 2 main elements in the Collapse component one the `toggle`, on clicking it we are calling the `expandCollapse function` and the other the collapse body on which we attach our ref, get it's height and spread the styles. 
So, if we want to create a hook, we need to basically export the config / props for the `toggle` and `collapse body` and that is what react collapse does. Along with that we will also export the `isExpanded` state and `expandCollapse` function so that we can toggle our collapse component using say an external button. We want to achieve something similar to the following - 
```typescript
import React from 'react'
import useCollapse from 'react-collapsed'

function App() {
  const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()

  return (
    <div>
      <button {...getToggleProps()}>
        {isExpanded ? 'Collapse' : 'Expand'}
      </button>
      <section {...getCollapseProps()}>Collapsed content</section>
    </div>
  )
}
``` 
- `getToggleProps()` which we spread on the toggle element like the `Button or Collapsible header` this function manages the `onClick()` for the Toggle element, calls the `expandCollapse function`. 
- `getCollapseProps()` which we spread on the `Collapsible body` so that we can animate its height transition it from closed to open, etc. 
- Finally, we have the `isExpaned` state of our collapsible.
Let's create a new file `use-collapse.ts` and paste - 
```typescript
import {
  useEffect,
  useState,
  CSSProperties,
  MouseEvent,
  useRef,
  TransitionEvent,
} from "react";
import { flushSync } from "react-dom";

import { callAll, getElementHeight, noop } from "./utils";

const easing = "cubic-bezier(0.4, 0, 0.2, 1)";

const duration = 266;

const collapsedStyles = {
  display: "none",
  height: "0px",
  overflow: "hidden",
};

type UseCollapseInput = {
  /** default state of the collapsible */
  isOpened?: boolean;
  /** triggered when the component is closing  */
  onCollapseStart?: () => void;
  /** triggered after the component is closed */
  onCollapseEnd?: () => void;
  /** triggered when the component is opening  */
  onExpandStart?: () => void;
  /** triggered after the component is opened */
  onExpandEnd?: () => void;
  /** triggered on open and close */
  onToggle?: () => void;
};

export function useCollapse({
  isOpened = false,
  onExpandStart = noop,
  onExpandEnd = noop,
  onCollapseStart = noop,
  onCollapseEnd = noop,
}: UseCollapseInput = {}) {
  const [isExpanded, setExpanded] = useState(isOpened);

  const [collapsibleStyles, setStylesRaw] = useState<CSSProperties>(
    isOpened ? {} : collapsedStyles
  );

  const containerRef = useRef<HTMLElement | null>(null);

  const mounted = useRef<boolean>(false);

  useEffect(() => {
    if (mounted.current) {
      expandCollapse(isOpened);
    }
    mounted.current = true;
  }, [isOpened]);

  const setStyles = (newStyles: {} | ((oldStyles: {}) => {})) => {
    flushSync(() => {
      setStylesRaw(newStyles);
    });
  };

  const mergeStyles = (newStyles: {}) => {
    setStyles((oldStyles) => ({ ...oldStyles, ...newStyles }));
  };

  const handleTransitionEnd = (e: React.TransitionEvent): void => {
    if (e.target !== containerRef.current || e.propertyName !== "height") {
      return;
    }

    if (isExpanded) {
      const height = getElementHeight(containerRef);

      // If the height at the end of the transition
      // matches the height we're animating to,
      if (height === collapsibleStyles.height) {
        setStyles({});
      } else {
        // If the heights don't match, this could be due the height of the content changing mid-transition
        mergeStyles({ height });
      }

      onExpandEnd();
    }

    onCollapseEnd();
  };

  function expandCollapse(nextState: boolean) {
    setExpanded(nextState);
    if (nextState) {
      requestAnimationFrame(() => {
        onExpandStart();
        mergeStyles({
          willChange: "height",
          display: "block",
          overflow: "hidden",
        });

        requestAnimationFrame(() => {
          const height = getElementHeight(containerRef);
          mergeStyles({
            transition: `height ${duration}ms ${easing}`,
            height,
          });
        });
      });
    } else {
      requestAnimationFrame(() => {
        onCollapseStart();
        const height = getElementHeight(containerRef);
        mergeStyles({
          transition: `height ${duration}ms ${easing}`,
          willChange: "height",
          height,
        });

        requestAnimationFrame(() => {
          mergeStyles({
            height: "0px",
            overflow: "hidden",
          });
        });
      });
    }
  }

  function getToggleProps({
    disabled = false,
    onClick = noop,
    ...rest
  }: GetTogglePropsInput = {}): GetTogglePropsOutput {
    return {
      type: "button",
      role: "button",
      id: `react-collapsed-toggle`,
      "aria-controls": `react-collapsed-panel`,
      "aria-expanded": isExpanded,
      tabIndex: 0,
      disabled,
      ...rest,
      onClick: disabled
        ? noop
        : callAll(() => expandCollapse(!isExpanded), onClick),
    };
  }

  function getCollapseProps({
    style = {},
    onTransitionEnd = noop,
    refKey = "ref",
    ...rest
  }: GetCollapsePropsInput = {}): GetCollapsePropsOutput {
    return {
      id: `react-collapsed-panel`,
      "aria-hidden": !isExpanded,
      [refKey]: containerRef,
      ...rest,
      onTransitionEnd: callAll(handleTransitionEnd, onTransitionEnd),
      style: {
        boxSizing: "border-box",
        // additional styles passed, e.g. getCollapseProps({style: {}})
        ...style,
        // style overrides from state
        ...collapsibleStyles,
      },
    };
  }

  return {
    getToggleProps,
    getCollapseProps,
    isExpanded,
    toggle: () => expandCollapse(!isExpanded),
  };
}

type ButtonType = "submit" | "reset" | "button";

export interface GetTogglePropsOutput {
  disabled: boolean;
  type: ButtonType;
  role: string;
  id: string;
  "aria-controls": string;
  "aria-expanded": boolean;
  tabIndex: number;
  onClick: (e: MouseEvent) => void;
}

export interface GetTogglePropsInput {
  [key: string]: unknown;
  disabled?: boolean;
  refKey?: string;
  onClick?: (e: MouseEvent) => void;
}

export interface GetCollapsePropsOutput {
  id: string;
  onTransitionEnd: (e: TransitionEvent) => void;
  style: CSSProperties;
  "aria-hidden": boolean;
}

export interface GetCollapsePropsInput {
  [key: string]: unknown;
  style?: CSSProperties;
  refKey?: string;
  onTransitionEnd?: (e: TransitionEvent) => void;
}
```
- `getToggleProps` function we will be spread on the `Toggle element`. On clicking the element, the `Collapsible element` should open and close. Therefore, we have to call `expandCollapse function` in the `onClick` of the `Toggle element`, this we handle in the hook.
- Another thing is that we can also pass an onClick handler to the `getToggleProps`, if want to execute some more code, eg. log the clicks.
- Whatever we are handling internally in the hook, an example been the styles of the `Collapsible` we are also giving an option for the user to pass in additional styles if he wants. You can see the `getCollapse function` also takes in optional styles.

Now under `App.tsx` paste the following - 
```typescript
import React from "react";

import { useCollapse } from "./use-collapse";

const collapseHeaderStyles: React.CSSProperties = {
  boxSizing: "border-box",
  border: "2px solid black",
  color: "#212121",
  fontFamily: "Helvetica",
  padding: "12px",
  fontSize: "16px",
  cursor: "pointer",
};

const collapseBodyStyles: React.CSSProperties = {
  boxSizing: "border-box",
  border: "2px solid black",
  borderTop: "none",
  color: "#212121",
  fontFamily: "Helvetica",
  padding: "12px",
  fontSize: "16px",
};

export function App() {
  const { getCollapseProps, getToggleProps, isExpanded } = useCollapse();

  return (
    <div>
      <div style={collapseHeaderStyles} {...getToggleProps({})}>
        {isExpanded ? "Close" : "Open"}
      </div>
      <div {...getCollapseProps()}>
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis
          impedit, quibusdam sapiente voluptatum doloremque laudantium, vel
          porro incidunt quidem dolores quis nostrum laboriosam suscipit ut vero
          molestias obcaecati accusantium culpa reiciendis optio. Autem, vero
          ex.
        </div>
      </div>
    </div>
  );
}
```

Toggle the Collapsible using an External Button  - 
```typescript
export function App() {
  const { getCollapseProps, getToggleProps, isExpanded, toggle } =
    useCollapse();

  return (
    <div>
      <button onClick={toggle}>
        {isExpanded ? "Close Collapsible" : "Open Collapsible"}
      </button>
      <div style={collapseHeaderStyles} {...getToggleProps({})}>
        {isExpanded ? "Close" : "Open"}
      </div>
      <div {...getCollapseProps()}>
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis
          impedit, quibusdam sapiente voluptatum doloremque laudantium, vel
          porro incidunt quidem dolores quis nostrum laboriosam suscipit ut vero
          molestias obcaecati accusantium culpa reiciendis optio. Autem, vero
          ex.
        </div>
      </div>
    </div>
  );
}
```
Sometimes we might need to control the Collapsible component using an external state altogether for such uses cases - 
```typescript
export function App() {
  const [isExpanded, setIsExpanded] = React.useState(false);

  const { getCollapseProps, getToggleProps } = useCollapse({
    isOpened: isExpanded,
  });

  function toggleCollapsible() {
    setIsExpanded((prevState) => !prevState);
  }

  return (
    <div>
      <button onClick={toggleCollapsible}>
        {isExpanded ? "Close Collapsible" : "Open Collapsible"}
      </button>
      <div
        style={collapseHeaderStyles}
        {...getToggleProps({
          onClick: toggleCollapsible,
        })}
      >
        {isExpanded ? "Close" : "Open"}
      </div>
      <div {...getCollapseProps()}>
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis
          impedit, quibusdam sapiente voluptatum doloremque laudantium, vel
          porro incidunt quidem dolores quis nostrum laboriosam suscipit ut vero
          molestias obcaecati accusantium culpa reiciendis optio. Autem, vero
          ex.
        </div>
      </div>
    </div>
  );
}
```
- Take a note that we have to pass the `onClick` function to the `getToggleProps`. When we click on the `Toggle` it will change the state of the hook (internal state) to say `true` using the `expandCollapse` function this we are managing internally in the hook. 
- But the App component's state is still `false`. Therefore, we passed an `onClick` handler to `getToggleProps` in order to sync the hook's internal state with App component's state.

Nested Collapsible Example - 
```typescript
function Nested() {
  const { isExpanded, getCollapseProps, getToggleProps } = useCollapse({
    isOpened: true,
  });

  return (
    <div>
      <div style={collapseHeaderStyles} {...getToggleProps()}>
        {isExpanded ? "Close" : "Open"}
      </div>
      <div {...getCollapseProps()}>
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis
          impedit, quibusdam sapiente voluptatum doloremque laudantium, vel
          porro incidunt quidem dolores quis nostrum laboriosam suscipit ut vero
          molestias obcaecati accusantium culpa reiciendis optio. Autem, vero
          ex.
        </div>
      </div>
    </div>
  );
}

export function App() {
  const { isExpanded, getCollapseProps, getToggleProps } = useCollapse({
    isOpened: true,
  });

  return (
    <div>
      <div style={collapseHeaderStyles} {...getToggleProps()}>
        {isExpanded ? "Close" : "Open"}
      </div>
      <div {...getCollapseProps()}>
        <div style={collapseBodyStyles}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis
          impedit, quibusdam sapiente voluptatum doloremque laudantium, vel
          porro incidunt quidem dolores quis nostrum laboriosam suscipit ut vero
          molestias obcaecati accusantium culpa reiciendis optio. Autem, vero
          ex.
          <Nested />
        </div>
      </div>
    </div>
  );
}
```
- The code that we have under `handleTransitionEnd` inside of the hook, fixes some nested collapsible issues.

### Summary
In this tutorial we first created a `Collapsible` component and then refactored it into a generic hook. Let me know if you have any questions / suggestions. Special shout out to the maintainers and collaborators of react-collapsed library. Until next time PEACE.