@lcashe/react-modal-controller
v1.0.1
Published
Tiny type-safe modal controller for React with inferred props and no registry.
Downloads
171
Maintainers
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
- React Setup
- Next.js Setup
- Important
- Simple Example
- Modal With Props
- TypeScript Inference
- Dynamic Initial Props
- Auto Close Example
- Multiple Modals
- Multiple Same Modals
- API
Installation
npm install @lcashe/react-modal-controllerReact 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 modalModals 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
