npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lcashe/react-modal-controller

v1.0.1

Published

Tiny type-safe modal controller for React with inferred props and no registry.

Downloads

171

Readme

react-modal-controller

Tiny and type-safe modal controller for React.

Works with React, Next.js and other React-based frameworks.

  • No global modal registry
  • No reducers or external state managers
  • No boilerplate
  • Automatic TypeScript prop inference

Navigation


Installation

npm install @lcashe/react-modal-controller

React Setup

Wrap your application with ModalScope.

import ReactDOM from 'react-dom/client';

import { ModalScope } from '@lcashe/react-modal-controller';

import { App } from './app';

ReactDOM.createRoot(document.getElementById('root')!).render(
   <ModalScope>
      <App />
   </ModalScope>,
);

ModalScope stores and renders all active modals internally.

Every modal opened through useModalController will automatically appear inside the nearest ModalScope.


Next.js Setup

App Router

app/providers.tsx

'use client';

import { PropsWithChildren } from 'react';

import { ModalScope } from '@lcashe/react-modal-controller';

export const Providers = ({ children }: PropsWithChildren) => {
   return <ModalScope>{children}</ModalScope>;
};

app/layout.tsx

import { Providers } from './providers';

export default function RootLayout({
   children,
}: {
   children: React.ReactNode;
}) {
   return (
      <html lang="en">
         <body>
            <Providers>{children}</Providers>
         </body>
      </html>
   );
}

Pages Router

pages/_app.tsx

import type { AppProps } from 'next/app';

import { ModalScope } from '@lcashe/react-modal-controller';

export default function App({ Component, pageProps }: AppProps) {
   return (
      <ModalScope>
         <Component {...pageProps} />
      </ModalScope>
   );
}

Important

Your modal component must accept:

opened: boolean;
onClose: VoidFunction;

Optional:

onExited?: VoidFunction;

Example:

type ModalProps = {
   opened: boolean;
   title: string;
   onClose: VoidFunction;
};

These props are injected automatically by the controller.


Simple Example

Modal

'use client';

type ModalProps = {
   opened: boolean;
   onClose: VoidFunction;
};

export const Modal = ({ opened, onClose }: ModalProps) => {
   if (!opened) {
      return null;
   }

   return (
      <div>
         <h1>Modal</h1>

         <button onClick={onClose}>Close</button>
      </div>
   );
};

Usage

'use client';

import { useModalController } from '@lcashe/react-modal-controller';

import { Modal } from './modal';

export const Component = () => {
   const modal = useModalController(Modal);

   return <button onClick={() => modal.open()}>Open modal</button>;
};

Modal With Props

Modal

'use client';

type ModalProps = {
   opened: boolean;
   title: string;
   onClose: VoidFunction;
};

export const Modal = ({ title, opened, onClose }: ModalProps) => {
   if (!opened) {
      return null;
   }

   return (
      <div>
         <h1>{title}</h1>

         <button onClick={onClose}>Close</button>
      </div>
   );
};

Usage

'use client';

import { useModalController } from '@lcashe/react-modal-controller';

import { Modal } from './modal';

export const Component = () => {
   const modal = useModalController(Modal, {
      title: 'Initial title',
   });

   return (
      <button
         onClick={() =>
            modal.open({
               title: 'Another title',
            })
         }
      >
         Open modal
      </button>
   );
};

initialProps are merged with props passed into open().

const modal = useModalController(Modal, {
   title: 'Default title',
});

modal.open({
   title: 'Another title',
});

The second object overrides the first one.


TypeScript Inference

Props are inferred automatically.

modal.open({
   title: 'Example title',
});

Autocomplete works out of the box.


Dynamic Initial Props

initialProps stay synchronized automatically.

const modal = useModalController(Modal, {
   title,
});

When title changes, the next open() call will use the latest value.

This makes it safe to use reactive values:

const modal = useModalController(Modal, {
   title: currentTitle,
});

The controller always keeps the latest values internally.


Auto Close Example

'use client';

import { useEffect } from 'react';

import { useModalController } from '@lcashe/react-modal-controller';

import { Modal } from './modal';

export const Component = () => {
   const modal = useModalController(Modal);

   useEffect(() => {
      modal.open();

      const timeoutId = setTimeout(() => {
         modal.close();
      }, 2000);

      return () => {
         clearTimeout(timeoutId);
      };
   }, [modal]);

   return null;
};

Multiple Modals

const firstModal = useModalController(FirstModal);

const secondModal = useModalController(SecondModal);

const thirdModal = useModalController(ThirdModal);

Multiple Same Modals

Each useModalController call creates its own local modal instance.

Even if you pass the same modal component, every controller receives a unique internal id, so the modals are controlled separately.

const firstModal = useModalController(Modal);

const secondModal = useModalController(Modal);

...

secondModal.open(); // opens only the second modal

Modals are mounted globally inside ModalScope, but every controller manages its own isolated modal state.


API

useModalController(Component, initialProps?)

Returns:

{
   open,
   close,
   remove,
}

open(props?)

Open modal with optional props.

modal.open();

modal.open({
   title: 'New title',
});

close()

Close modal.

modal.close();

remove()

Remove modal from the store completely.

modal.remove();

Usually useful when you want to completely destroy modal state manually.


License

MIT