✨ Introducing react-cool-portal: React hook for Portals, which renders modals, dropdowns, tooltips etc. to <body> or else
Tags: react, hook, opensource, javascript
Published: 2020-03-29 15:20:06

Yo guys, this is a React [hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook) for [Portals](https://reactjs.org/docs/portals.html). It helps you render children into a DOM node that exists outside the DOM hierarchy of the parent component. From now on you will never need to struggle with modals, dropdowns, tooltips etc. Check the [features](#features) section out to learn more. Hope you guys 👍🏻 it.

![demo](https://user-images.githubusercontent.com/21308003/91049364-b1eb1580-e64f-11ea-9776-4668551db40d.gif)

⚡️ Try yourself: https://react-cool-portal.netlify.app

## Features

- 🍒 Renders an element or component to `<body>` or [a specified DOM element](#basic-use-case).
- 🎣 React [Portals](https://reactjs.org/docs/portals.html) feat. [Hook](https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook).
- 🤖 Built-in [state controllers](#use-with-state), event listeners and many [useful features](https://github.com/wellyshen/react-cool-portal#api) for a comprehensive DX.
- 🧱 Used as a scaffold to [build your customized hook](#build-your-customized-hook).
- 🧹 Auto removes the un-used portal container for you. Doesn't produce any DOM mess.
- 📜 Supports [TypeScript](https://www.typescriptlang.org) type definition.
- 🗄️ Server-side rendering compatibility.
- 🦔 Tiny size ([~ 1KB gzipped](https://bundlephobia.com/result?p=react-cool-portal)). No external dependencies, aside for the `react` and `react-dom`.

## Usage

Here are some minimal examples of how does it work. You can learn more about it by checking the [API](https://github.com/wellyshen/react-cool-portal#api) out.

### Basic Use Case

Inserts an element or component into a different location in the DOM.

```js
import usePortal from 'react-cool-portal';

const App = () => {
  const { Portal } = usePortal();

  return (
    <div>
      <Portal>
        <p>
          Wow! I am rendered outside the DOM hierarchy of my parent component.
        </p>
      </Portal>
    </div>
  );
};
```

By default, the children of portal is rendered into `<div id="react-cool-portal">` of `<body>`. You can specify the DOM element you want through the `containerId` option.

```js
import usePortal from 'react-cool-portal';

const App = () => {
  const { Portal } = usePortal({ containerId: 'my-portal-root' });

  return (
    <div>
      <Portal>
        <p>Now I am rendered into the specify element (id="my-portal-root").</p>
      </Portal>
    </div>
  );
};
```

> Note: If the container element doesn't exist, we will create it for you.

### Use with State

`react-cool-portal` provides many useful features, which enable you to build a component with state. For instance, modal, dropdown, tooltip, and so on.

```js
import usePortal from 'react-cool-portal';

const App = () => {
  const { Portal, isShow, show, hide, toggle } = usePortal({
    defaultShow: false, // The default visibility of portal, default is true
    onShow: e => {
      // Triggered when portal is shown
      // The event object will be the parameter of "show(e?)"
    },
    onHide: e => {
      // Triggered when portal is hidden
      // The event object will be the parameter of "hide(e?)", it maybe MouseEvent (on clicks outside) or KeyboardEvent (press ESC key)
    }
  });

  return (
    <div>
      <button onClick={show}>Open Modal</button>
      <button onClick={hide}>Close Modal</button>
      <button onClick={toggle}>{isShow ? 'Close' : 'Open'} Modal</button>
      <Portal>
        <div class="modal" tabIndex={-1}>
          <div
            class="modal-dialog"
            role="dialog"
            aria-labelledby="modal-label"
            aria-modal="true"
          >
            <div class="modal-header">
              <h5 id="modal-label" class="modal-title">
                Modal title
              </h5>
            </div>
            <div class="modal-body">
              <p>Modal body text goes here.</p>
            </div>
          </div>
        </div>
      </Portal>
    </div>
  );
};
```

> 🧹 When no element in the container, we will remove it for you to avoid DOM mess.

The above example shows how easy you can handle the visibility of your component. You may ask how to handle the visibility with animations? No worries, you can disable the built-in `show/hide` functions by setting the `internalShowHide` option as `false` then handling the visibility of your component via the `isShow` state.

```js
import usePortal from 'react-cool-portal';

const App = () => {
  const { Portal, isShow, show, hide, toggle } = usePortal({
    defaultShow: false,
    internalShowHide: false, // Disable the built-in show/hide portal functions, default is true
    onShow: e => {
      // Triggered when "isShow" is set as true
    },
    onHide: e => {
      // Triggered when "isShow" is set as false
    }
  });

  return (
    <div>
      <button onClick={show}>Open Modal</button>
      <button onClick={hide}>Close Modal</button>
      <button onClick={toggle}>{isShow ? 'Close' : 'Open'} Modal</button>
      <Portal>
        <div
          // Now you can use the "isShow" state to handle the CSS animations
          class={`modal${isShow ? ' modal-open' : ''}`}
          tabIndex={-1}
        >
          <div
            class="modal-dialog"
            role="dialog"
            aria-labelledby="modal-label"
            aria-modal="true"
          >
            <div class="modal-header">
              <h5 id="modal-label" class="modal-title">
                Modal title
              </h5>
            </div>
            <div class="modal-body">
              <p>Modal body text goes here.</p>
            </div>
          </div>
        </div>
      </Portal>
    </div>
  );
};
```

Besides that, you can also handle the visibility of your component via React [animation events](https://reactjs.org/docs/events.html#animation-events) or [translation events](https://reactjs.org/docs/events.html#transition-events) like [what I did](https://github.com/wellyshen/react-cool-portal/blob/master/demo/App/index.tsx) for the [demo app](https://github.com/wellyshen/react-cool-portal#live-demo).

### Build Your Customized Hook

Are you tired to write the same code over and over again? It's time to build your own hook based on `react-cool-portal` then use it wherever you want.

```js
import { useCallback } from 'react';
import usePortal from 'react-cool-portal';

// Customize your hook based on react-cool-portal
const useModal = (options = {}) => {
  const { Portal, isShow, ...rest } = usePortal({
    ...options,
    defaultShow: false,
    internalShowHide: false
  });

  const Modal = useCallback(
    ({ children }) => (
      <Portal>
        <div class={`modal${isShow ? ' modal-open' : ''}`} tabIndex={-1}>
          {children}
        </div>
      </Portal>
    ),
    []
  );

  return { Modal, isShow, ...rest };
};

// Use it wherever you want
const App = () => {
  const { Modal, show, hide } = useModal();

  return (
    <div>
      <button onClick={show}>Open Modal</button>
      <button onClick={hide}>Close Modal</button>
      <Modal>
        <div
          class="modal-dialog"
          role="dialog"
          aria-labelledby="modal-label"
          aria-modal="true"
        >
          <div class="modal-header">
            <h5 id="modal-label" class="modal-title">
              Modal title
            </h5>
          </div>
          <div class="modal-body">
            <p>Modal body text goes here.</p>
          </div>
        </div>
      </Modal>
    </div>
  );
};
```

---

Thanks for reading, for more usage details checkout the project's GitHub page: https://github.com/wellyshen/react-cool-portal

You can also install this package is distributed via [npm](https://www.npmjs.com/package/react-cool-portal).

```sh
$ yarn add react-cool-portal
# or
$ npm install --save react-cool-portal
```