= () => {
+ const [present] = useIonLoading();
+ return (
+
+
+
+ present({
+ duration: 3000,
+ })
+ }
+ >
+ Show Loading
+
+ present('Loading', 2000, 'dots')}
+ >
+ Show Loading using params
+
+
+
+ );
+};
+```
+
+```tsx
+/* Using with IonLoading Component */
+
import React, { useState } from 'react';
import { IonLoading, IonButton, IonContent } from '@ionic/react';
diff --git a/core/src/components/modal/readme.md b/core/src/components/modal/readme.md
index 9961776372..af3bee1008 100644
--- a/core/src/components/modal/readme.md
+++ b/core/src/components/modal/readme.md
@@ -332,6 +332,70 @@ modalElement.presentingElement = await modalController.getTop(); // Get the top-
### React
```tsx
+/* Using with useIonModal Hook */
+
+import React, { useState } from 'react';
+import { IonButton, IonContent, IonPage, useIonModal } from '@ionic/react';
+
+const Body: React.FC<{
+ count: number;
+ onDismiss: () => void;
+ onIncrement: () => void;
+}> = ({ count, onDismiss, onIncrement }) => (
+
+ count: {count}
+ onIncrement()}>
+ Increment Count
+
+ onDismiss()}>
+ Close
+
+
+);
+
+const ModalExample: React.FC = () => {
+ const [count, setCount] = useState(0);
+
+ const handleIncrement = () => {
+ setCount(count + 1);
+ };
+
+ const handleDismiss = () => {
+ dismiss();
+ };
+
+ /**
+ * First parameter is the component to show, second is the props to pass
+ */
+ const [present, dismiss] = useIonModal(Body, {
+ count,
+ onDismiss: handleDismiss,
+ onIncrement: handleIncrement,
+ });
+
+ return (
+
+
+ {
+ present({
+ cssClass: 'my-class',
+ });
+ }}
+ >
+ Show Modal
+
+ Count: {count}
+
+
+ );
+};
+```
+
+```tsx
+/* Using with IonModal Component */
+
import React, { useState } from 'react';
import { IonModal, IonButton, IonContent } from '@ionic/react';
diff --git a/core/src/components/modal/usage/react.md b/core/src/components/modal/usage/react.md
index 71926dc662..5dc0d651db 100644
--- a/core/src/components/modal/usage/react.md
+++ b/core/src/components/modal/usage/react.md
@@ -1,4 +1,68 @@
```tsx
+/* Using with useIonModal Hook */
+
+import React, { useState } from 'react';
+import { IonButton, IonContent, IonPage, useIonModal } from '@ionic/react';
+
+const Body: React.FC<{
+ count: number;
+ onDismiss: () => void;
+ onIncrement: () => void;
+}> = ({ count, onDismiss, onIncrement }) => (
+
+ count: {count}
+ onIncrement()}>
+ Increment Count
+
+ onDismiss()}>
+ Close
+
+
+);
+
+const ModalExample: React.FC = () => {
+ const [count, setCount] = useState(0);
+
+ const handleIncrement = () => {
+ setCount(count + 1);
+ };
+
+ const handleDismiss = () => {
+ dismiss();
+ };
+
+ /**
+ * First parameter is the component to show, second is the props to pass
+ */
+ const [present, dismiss] = useIonModal(Body, {
+ count,
+ onDismiss: handleDismiss,
+ onIncrement: handleIncrement,
+ });
+
+ return (
+
+
+ {
+ present({
+ cssClass: 'my-class',
+ });
+ }}
+ >
+ Show Modal
+
+ Count: {count}
+
+
+ );
+};
+```
+
+```tsx
+/* Using with IonModal Component */
+
import React, { useState } from 'react';
import { IonModal, IonButton, IonContent } from '@ionic/react';
diff --git a/core/src/components/picker/readme.md b/core/src/components/picker/readme.md
index 0e7513a954..bcae3a1a86 100644
--- a/core/src/components/picker/readme.md
+++ b/core/src/components/picker/readme.md
@@ -7,6 +7,95 @@ A Picker is a dialog that displays a row of buttons and columns underneath. It a
+## Usage
+
+### React
+
+```tsx
+/* Using with useIonPicker Hook */
+
+import React, { useState } from 'react';
+import { IonButton, IonContent, IonPage, useIonPicker } from '@ionic/react';
+
+const PickerExample: React.FC = () => {
+ const [present] = useIonPicker();
+ const [value, setValue] = useState('');
+ return (
+
+
+
+ present({
+ buttons: [
+ {
+ text: 'Confirm',
+ handler: (selected) => {
+ setValue(selected.animal.value)
+ },
+ },
+ ],
+ columns: [
+ {
+ name: 'animal',
+ options: [
+ { text: 'Dog', value: 'dog' },
+ { text: 'Cat', value: 'cat' },
+ { text: 'Bird', value: 'bird' },
+ ],
+ },
+ ],
+ })
+ }
+ >
+ Show Picker
+
+
+ present(
+ [
+ {
+ name: 'animal',
+ options: [
+ { text: 'Dog', value: 'dog' },
+ { text: 'Cat', value: 'cat' },
+ { text: 'Bird', value: 'bird' },
+ ],
+ },
+ {
+ name: 'vehicle',
+ options: [
+ { text: 'Car', value: 'car' },
+ { text: 'Truck', value: 'truck' },
+ { text: 'Bike', value: 'bike' },
+ ],
+ },
+ ],
+ [
+ {
+ text: 'Confirm',
+ handler: (selected) => {
+ setValue(`${selected.animal.value}, ${selected.vehicle.value}`)
+ },
+ },
+ ]
+ )
+ }
+ >
+ Show Picker using params
+
+ {value && (
+ Selected Value: {value}
+ )}
+
+
+ );
+};
+```
+
+
+
## Properties
| Property | Attribute | Description | Type | Default |
diff --git a/core/src/components/picker/usage/react.md b/core/src/components/picker/usage/react.md
new file mode 100644
index 0000000000..1d016decba
--- /dev/null
+++ b/core/src/components/picker/usage/react.md
@@ -0,0 +1,82 @@
+```tsx
+/* Using with useIonPicker Hook */
+
+import React, { useState } from 'react';
+import { IonButton, IonContent, IonPage, useIonPicker } from '@ionic/react';
+
+const PickerExample: React.FC = () => {
+ const [present] = useIonPicker();
+ const [value, setValue] = useState('');
+ return (
+
+
+
+ present({
+ buttons: [
+ {
+ text: 'Confirm',
+ handler: (selected) => {
+ setValue(selected.animal.value)
+ },
+ },
+ ],
+ columns: [
+ {
+ name: 'animal',
+ options: [
+ { text: 'Dog', value: 'dog' },
+ { text: 'Cat', value: 'cat' },
+ { text: 'Bird', value: 'bird' },
+ ],
+ },
+ ],
+ })
+ }
+ >
+ Show Picker
+
+
+ present(
+ [
+ {
+ name: 'animal',
+ options: [
+ { text: 'Dog', value: 'dog' },
+ { text: 'Cat', value: 'cat' },
+ { text: 'Bird', value: 'bird' },
+ ],
+ },
+ {
+ name: 'vehicle',
+ options: [
+ { text: 'Car', value: 'car' },
+ { text: 'Truck', value: 'truck' },
+ { text: 'Bike', value: 'bike' },
+ ],
+ },
+ ],
+ [
+ {
+ text: 'Confirm',
+ handler: (selected) => {
+ setValue(`${selected.animal.value}, ${selected.vehicle.value}`)
+ },
+ },
+ ]
+ )
+ }
+ >
+ Show Picker using params
+
+ {value && (
+ Selected Value: {value}
+ )}
+
+
+ );
+};
+```
\ No newline at end of file
diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md
index b8fbd9b1b1..b62156f9db 100644
--- a/core/src/components/popover/readme.md
+++ b/core/src/components/popover/readme.md
@@ -114,6 +114,59 @@ function presentPopover(ev) {
### React
```tsx
+/* Using with useIonPopover Hook */
+
+import React from 'react';
+import {
+ IonButton,
+ IonContent,
+ IonItem,
+ IonList,
+ IonListHeader,
+ IonPage,
+ useIonPopover,
+} from '@ionic/react';
+
+const PopoverList: React.FC<{
+ onHide: () => void;
+}> = ({ onHide }) => (
+
+ Ionic
+ Learn Ionic
+ Documentation
+ Showcase
+ GitHub Repo
+
+ Close
+
+
+);
+
+const PopoverExample: React.FC = () => {
+ const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() });
+
+ return (
+
+
+
+ present({
+ event: e.nativeEvent,
+ })
+ }
+ >
+ Show Popover
+
+
+
+ );
+};
+```
+
+```tsx
+/* Using with IonPopover Component */
+
import React, { useState } from 'react';
import { IonPopover, IonButton } from '@ionic/react';
diff --git a/core/src/components/popover/usage/react.md b/core/src/components/popover/usage/react.md
index f55b109f52..e0d444e5dd 100644
--- a/core/src/components/popover/usage/react.md
+++ b/core/src/components/popover/usage/react.md
@@ -1,4 +1,57 @@
```tsx
+/* Using with useIonPopover Hook */
+
+import React from 'react';
+import {
+ IonButton,
+ IonContent,
+ IonItem,
+ IonList,
+ IonListHeader,
+ IonPage,
+ useIonPopover,
+} from '@ionic/react';
+
+const PopoverList: React.FC<{
+ onHide: () => void;
+}> = ({ onHide }) => (
+
+ Ionic
+ Learn Ionic
+ Documentation
+ Showcase
+ GitHub Repo
+
+ Close
+
+
+);
+
+const PopoverExample: React.FC = () => {
+ const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() });
+
+ return (
+
+
+
+ present({
+ event: e.nativeEvent,
+ })
+ }
+ >
+ Show Popover
+
+
+
+ );
+};
+```
+
+```tsx
+/* Using with IonPopover Component */
+
import React, { useState } from 'react';
import { IonPopover, IonButton } from '@ionic/react';
diff --git a/core/src/components/toast/readme.md b/core/src/components/toast/readme.md
index f29b1c03d8..9ca8a37938 100644
--- a/core/src/components/toast/readme.md
+++ b/core/src/components/toast/readme.md
@@ -111,6 +111,48 @@ async function presentToastWithOptions() {
### React
```tsx
+/* Using the useIonToast Hook */
+
+import React from 'react';
+import { IonButton, IonContent, IonPage, useIonToast } from '@ionic/react';
+
+const ToastExample: React.FC = () => {
+ const [present, dismiss] = useIonToast();
+
+ return (
+
+
+
+ present({
+ buttons: [{ text: 'hide', handler: () => dismiss() }],
+ message: 'toast from hook, click hide to dismiss',
+ onDidDismiss: () => console.log('dismissed'),
+ onWillDismiss: () => console.log('will dismiss'),
+ })
+ }
+ >
+ Show Toast
+
+ present('hello from hook', 3000)}
+ >
+ Show Toast using params, closes in 3 secs
+
+
+ Hide Toast
+
+
+
+ );
+};
+```
+
+```tsx
+/* Using the IonToast Component */
+
import React, { useState } from 'react';
import { IonToast, IonContent, IonButton } from '@ionic/react';
diff --git a/core/src/components/toast/usage/react.md b/core/src/components/toast/usage/react.md
index 5aad0cf5df..82367cc525 100644
--- a/core/src/components/toast/usage/react.md
+++ b/core/src/components/toast/usage/react.md
@@ -1,4 +1,46 @@
```tsx
+/* Using the useIonToast Hook */
+
+import React from 'react';
+import { IonButton, IonContent, IonPage, useIonToast } from '@ionic/react';
+
+const ToastExample: React.FC = () => {
+ const [present, dismiss] = useIonToast();
+
+ return (
+
+
+
+ present({
+ buttons: [{ text: 'hide', handler: () => dismiss() }],
+ message: 'toast from hook, click hide to dismiss',
+ onDidDismiss: () => console.log('dismissed'),
+ onWillDismiss: () => console.log('will dismiss'),
+ })
+ }
+ >
+ Show Toast
+
+ present('hello from hook', 3000)}
+ >
+ Show Toast using params, closes in 3 secs
+
+
+ Hide Toast
+
+
+
+ );
+};
+```
+
+```tsx
+/* Using the IonToast Component */
+
import React, { useState } from 'react';
import { IonToast, IonContent, IonButton } from '@ionic/react';
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 39acf1346c..0395ab811a 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -69,6 +69,15 @@ export * from './hrefprops';
// Ionic Animations
export { CreateAnimation } from './CreateAnimation';
+// Hooks
+export { useIonActionSheet, UseIonActionSheetResult } from '../hooks/useIonActionSheet';
+export { useIonAlert, UseIonAlertResult } from '../hooks/useIonAlert';
+export { useIonToast, UseIonToastResult } from '../hooks/useIonToast';
+export { useIonModal, UseIonModalResult } from '../hooks/useIonModal';
+export { useIonPopover, UseIonPopoverResult } from '../hooks/useIonPopover';
+export { useIonPicker, UseIonPickerResult } from '../hooks/useIonPicker';
+export { useIonLoading, UseIonLoadingResult } from '../hooks/useIonLoading';
+
// Icons that are used by internal components
addIcons({
'arrow-back-sharp': arrowBackSharp,
diff --git a/packages/react/src/hooks/HookOverlayOptions.ts b/packages/react/src/hooks/HookOverlayOptions.ts
new file mode 100644
index 0000000000..431652758c
--- /dev/null
+++ b/packages/react/src/hooks/HookOverlayOptions.ts
@@ -0,0 +1,8 @@
+import { OverlayEventDetail } from '@ionic/core';
+
+export interface HookOverlayOptions {
+ onDidDismiss?: (event: CustomEvent) => void;
+ onDidPresent?: (event: CustomEvent) => void;
+ onWillDismiss?: (event: CustomEvent) => void;
+ onWillPresent?: (event: CustomEvent) => void;
+}
diff --git a/packages/react/src/hooks/useController.ts b/packages/react/src/hooks/useController.ts
new file mode 100644
index 0000000000..fa596f3704
--- /dev/null
+++ b/packages/react/src/hooks/useController.ts
@@ -0,0 +1,83 @@
+import { OverlayEventDetail } from '@ionic/core';
+import { useMemo, useRef } from 'react';
+
+import { attachProps } from '../components/utils';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+
+interface OverlayBase extends HTMLElement {
+ present: () => Promise;
+ dismiss: (data?: any, role?: string | undefined) => Promise;
+}
+
+export function useController<
+ OptionsType,
+ OverlayType extends OverlayBase
+>(
+ displayName: string,
+ controller: { create: (options: OptionsType) => Promise }
+) {
+ const overlayRef = useRef();
+ const didDismissEventName = useMemo(
+ () => `on${displayName}DidDismiss`,
+ [displayName]
+ );
+ const didPresentEventName = useMemo(
+ () => `on${displayName}DidPresent`,
+ [displayName]
+ );
+ const willDismissEventName = useMemo(
+ () => `on${displayName}WillDismiss`,
+ [displayName]
+ );
+ const willPresentEventName = useMemo(
+ () => `on${displayName}WillPresent`,
+ [displayName]
+ );
+
+ const present = async (options: OptionsType & HookOverlayOptions) => {
+ if (overlayRef.current) {
+ return;
+ }
+ const {
+ onDidDismiss,
+ onWillDismiss,
+ onDidPresent,
+ onWillPresent,
+ ...rest
+ } = options;
+
+ const handleDismiss = (event: CustomEvent>) => {
+ if (onDidDismiss) {
+ onDidDismiss(event);
+ }
+ overlayRef.current = undefined;
+ }
+
+ overlayRef.current = await controller.create({
+ ...(rest as any),
+ });
+
+ attachProps(overlayRef.current, {
+ [didDismissEventName]: handleDismiss,
+ [didPresentEventName]: (e: CustomEvent) =>
+ onDidPresent && onDidPresent(e),
+ [willDismissEventName]: (e: CustomEvent) =>
+ onWillDismiss && onWillDismiss(e),
+ [willPresentEventName]: (e: CustomEvent) =>
+ onWillPresent && onWillPresent(e),
+ });
+
+ overlayRef.current.present();
+ };
+
+ const dismiss = async () => {
+ overlayRef.current && await overlayRef.current.dismiss();
+ overlayRef.current = undefined;
+ };
+
+ return {
+ present,
+ dismiss,
+ };
+}
diff --git a/packages/react/src/hooks/useIonActionSheet.ts b/packages/react/src/hooks/useIonActionSheet.ts
new file mode 100644
index 0000000000..cbd045f4ef
--- /dev/null
+++ b/packages/react/src/hooks/useIonActionSheet.ts
@@ -0,0 +1,53 @@
+import { ActionSheetButton, ActionSheetOptions, actionSheetController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { useController } from './useController';
+
+/**
+ * A hook for presenting/dismissing an IonActionSheet component
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonActionSheet(): UseIonActionSheetResult {
+ const controller = useController(
+ 'IonActionSheet',
+ actionSheetController
+ );
+
+ function present(buttons: ActionSheetButton[], header?: string): void;
+ function present(options: ActionSheetOptions & HookOverlayOptions): void;
+ function present(buttonsOrOptions: ActionSheetButton[] | ActionSheetOptions & HookOverlayOptions, header?: string) {
+ if (Array.isArray(buttonsOrOptions)) {
+ controller.present({
+ buttons: buttonsOrOptions,
+ header
+ });
+ } else {
+ controller.present(buttonsOrOptions);
+ }
+ }
+
+ return [
+ present,
+ controller.dismiss
+ ];
+}
+
+export type UseIonActionSheetResult = [
+ {
+ /**
+ * Presents the action sheet
+ * @param buttons An array of buttons for the action sheet
+ * @param header Optional - Title for the action sheet
+ */
+ (buttons: ActionSheetButton[], header?: string | undefined): void;
+ /**
+ * Presents the action sheet
+ * @param options The options to pass to the IonActionSheet
+ */
+ (options: ActionSheetOptions & HookOverlayOptions): void;
+ },
+ /**
+ * Dismisses the action sheet
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonAlert.ts b/packages/react/src/hooks/useIonAlert.ts
new file mode 100644
index 0000000000..3f1f7cf7fc
--- /dev/null
+++ b/packages/react/src/hooks/useIonAlert.ts
@@ -0,0 +1,53 @@
+import { AlertButton, AlertOptions, alertController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { useController } from './useController';
+
+/**
+ * A hook for presenting/dismissing an IonAlert component
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonAlert(): UseIonAlertResult {
+ const controller = useController(
+ 'IonAlert',
+ alertController
+ );
+
+ function present(message: string, buttons?: AlertButton[]): void;
+ function present(options: AlertOptions & HookOverlayOptions): void;
+ function present(messageOrOptions: string | AlertOptions & HookOverlayOptions, buttons?: AlertButton[]) {
+ if (typeof messageOrOptions === 'string') {
+ controller.present({
+ message: messageOrOptions,
+ buttons: buttons ?? [{ text: 'Ok' }]
+ });
+ } else {
+ controller.present(messageOrOptions);
+ }
+ };
+
+ return [
+ present,
+ controller.dismiss
+ ];
+}
+
+export type UseIonAlertResult = [
+ {
+ /**
+ * Presents the alert
+ * @param message The main message to be displayed in the alert
+ * @param buttons Optional - Array of buttons to be added to the alert
+ */
+ (message: string, buttons?: AlertButton[]): void;
+ /**
+ * Presents the alert
+ * @param options The options to pass to the IonAlert
+ */
+ (options: AlertOptions & HookOverlayOptions): void;
+ },
+ /**
+ * Dismisses the alert
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonLoading.tsx b/packages/react/src/hooks/useIonLoading.tsx
new file mode 100644
index 0000000000..779a413c18
--- /dev/null
+++ b/packages/react/src/hooks/useIonLoading.tsx
@@ -0,0 +1,60 @@
+import { LoadingOptions, SpinnerTypes, loadingController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { useController } from './useController';
+
+/**
+ * A hook for presenting/dismissing an IonLoading component
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonLoading(): UseIonLoadingResult {
+ const controller = useController(
+ 'IonLoading',
+ loadingController
+ );
+
+ function present(
+ message?: string,
+ duration?: number,
+ spinner?: SpinnerTypes
+ ): void;
+ function present(options: LoadingOptions & HookOverlayOptions): void;
+ function present(
+ messageOrOptions: string | (LoadingOptions & HookOverlayOptions) = '',
+ duration?: number,
+ spinner?: SpinnerTypes
+ ) {
+ if (typeof messageOrOptions === 'string') {
+ controller.present({
+ message: messageOrOptions,
+ duration,
+ spinner: spinner ?? 'lines',
+ });
+ } else {
+ controller.present(messageOrOptions);
+ }
+ }
+
+ return [present, controller.dismiss];
+}
+
+export type UseIonLoadingResult = [
+ {
+ /**
+ * Presents the loading indicator
+ * @param message Optional - Text content to display in the loading indicator, defaults to blank string
+ * @param duration Optional - Number of milliseconds to wait before dismissing the loading indicator
+ * @param spinner Optional - The name of the spinner to display, defaults to "lines"
+ */
+ (message?: string, duration?: number, spinner?: SpinnerTypes): void;
+ /**
+ * Presents the loading indicator
+ * @param options The options to pass to the IonLoading
+ */
+ (options: LoadingOptions & HookOverlayOptions): void;
+ },
+ /**
+ * Dismisses the loading indicator
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonModal.ts b/packages/react/src/hooks/useIonModal.ts
new file mode 100644
index 0000000000..4a84969aca
--- /dev/null
+++ b/packages/react/src/hooks/useIonModal.ts
@@ -0,0 +1,36 @@
+import { ModalOptions, modalController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { ReactComponentOrElement, useOverlay } from './useOverlay';
+
+/**
+ * A hook for presenting/dismissing an IonModal component
+ * @param component The component that the modal will show. Can be a React Component, a functional component, or a JSX Element
+ * @param componentProps The props that will be passed to the component, if required
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonModal(component: ReactComponentOrElement, componentProps?: any): UseIonModalResult {
+ const controller = useOverlay(
+ 'IonModal',
+ modalController,
+ component,
+ componentProps
+ );
+
+ function present(options: Omit & HookOverlayOptions = {}) {
+ controller.present(options as any);
+ };
+
+ return [
+ present,
+ controller.dismiss
+ ];
+}
+
+export type UseIonModalResult = [
+ (options?: Omit & HookOverlayOptions) => void,
+ /**
+ * Dismisses the modal
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonPicker.tsx b/packages/react/src/hooks/useIonPicker.tsx
new file mode 100644
index 0000000000..36473862aa
--- /dev/null
+++ b/packages/react/src/hooks/useIonPicker.tsx
@@ -0,0 +1,58 @@
+import {
+ PickerButton,
+ PickerColumn,
+ PickerOptions,
+ pickerController,
+} from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { useController } from './useController';
+
+/**
+ * A hook for presenting/dismissing an IonPicker component
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonPicker(): UseIonPickerResult {
+ const controller = useController(
+ 'IonPicker',
+ pickerController
+ );
+
+ function present(columns: PickerColumn[], buttons?: PickerButton[]): void;
+ function present(options: PickerOptions & HookOverlayOptions): void;
+ function present(
+ columnsOrOptions: PickerColumn[] | (PickerOptions & HookOverlayOptions),
+ buttons?: PickerButton[]
+ ) {
+ if (Array.isArray(columnsOrOptions)) {
+ controller.present({
+ columns: columnsOrOptions,
+ buttons: buttons ?? [{ text: 'Ok' }],
+ });
+ } else {
+ controller.present(columnsOrOptions);
+ }
+ }
+
+ return [present, controller.dismiss];
+}
+
+export type UseIonPickerResult = [
+ {
+ /**
+ * Presents the picker
+ * @param columns Array of columns to be displayed in the picker.
+ * @param buttons Optional - Array of buttons to be displayed at the top of the picker.
+ */
+ (columns: PickerColumn[], buttons?: PickerButton[]): void;
+ /**
+ * Presents the picker
+ * @param options The options to pass to the IonPicker
+ */
+ (options: PickerOptions & HookOverlayOptions): void;
+ },
+ /**
+ * Dismisses the picker
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonPopover.ts b/packages/react/src/hooks/useIonPopover.ts
new file mode 100644
index 0000000000..42ecf6600c
--- /dev/null
+++ b/packages/react/src/hooks/useIonPopover.ts
@@ -0,0 +1,36 @@
+import { PopoverOptions, popoverController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { ReactComponentOrElement, useOverlay } from './useOverlay';
+
+/**
+ * A hook for presenting/dismissing an IonPicker component
+ * @param component The component that the popover will show. Can be a React Component, a functional component, or a JSX Element
+ * @param componentProps The props that will be passed to the component, if required
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonPopover(component: ReactComponentOrElement, componentProps?: any): UseIonPopoverResult {
+ const controller = useOverlay(
+ 'IonPopover',
+ popoverController,
+ component,
+ componentProps
+ );
+
+ function present(options: Omit & HookOverlayOptions = {}) {
+ controller.present(options as any);
+ };
+
+ return [
+ present,
+ controller.dismiss
+ ];
+}
+
+export type UseIonPopoverResult = [
+ (options?: Omit & HookOverlayOptions) => void,
+ /**
+ * Dismisses the popover
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useIonToast.ts b/packages/react/src/hooks/useIonToast.ts
new file mode 100644
index 0000000000..258d5b2bc8
--- /dev/null
+++ b/packages/react/src/hooks/useIonToast.ts
@@ -0,0 +1,53 @@
+import { ToastOptions, toastController } from '@ionic/core';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+import { useController } from './useController';
+
+/**
+ * A hook for presenting/dismissing an IonToast component
+ * @returns Returns the present and dismiss methods in an array
+ */
+export function useIonToast(): UseIonToastResult {
+ const controller = useController(
+ 'IonToast',
+ toastController
+ );
+
+ function present(message: string, duration?: number): void;
+ function present(options: ToastOptions & HookOverlayOptions): void;
+ function present(messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) {
+ if (typeof messageOrOptions === 'string') {
+ controller.present({
+ message: messageOrOptions,
+ duration
+ });
+ } else {
+ controller.present(messageOrOptions);
+ }
+ };
+
+ return [
+ present,
+ controller.dismiss
+ ];
+}
+
+export type UseIonToastResult = [
+ {
+ /**
+ * Presents the toast
+ * @param message Message to be shown in the toast.
+ * @param duration Optional - How many milliseconds to wait before hiding the toast. By default, it will show until dismissToast() is called.
+ */
+ (message: string, duration?: number): void;
+ /**
+ * Presents the Toast
+ * @param options The options to pass to the IonToast.
+ */
+ (options: ToastOptions & HookOverlayOptions): void;
+ },
+ /**
+ * Dismisses the toast
+ */
+ () => void
+];
diff --git a/packages/react/src/hooks/useOverlay.ts b/packages/react/src/hooks/useOverlay.ts
new file mode 100644
index 0000000000..6365bc4829
--- /dev/null
+++ b/packages/react/src/hooks/useOverlay.ts
@@ -0,0 +1,111 @@
+import { OverlayEventDetail } from '@ionic/core';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import ReactDOM from 'react-dom';
+
+import { attachProps } from '../components/utils';
+
+import { HookOverlayOptions } from './HookOverlayOptions';
+
+export type ReactComponentOrElement = React.ComponentClass | React.FC | JSX.Element;
+
+interface OverlayBase extends HTMLElement {
+ present: () => Promise;
+ dismiss: (data?: any, role?: string | undefined) => Promise;
+}
+
+export function useOverlay<
+ OptionsType,
+ OverlayType extends OverlayBase
+>(
+ displayName: string,
+ controller: { create: (options: OptionsType) => Promise; },
+ component: ReactComponentOrElement,
+ componentProps?: any
+) {
+ const overlayRef = useRef();
+ const containerElRef = useRef();
+ const didDismissEventName = useMemo(
+ () => `on${displayName}DidDismiss`,
+ [displayName]
+ );
+ const didPresentEventName = useMemo(
+ () => `on${displayName}DidPresent`,
+ [displayName]
+ );
+ const willDismissEventName = useMemo(
+ () => `on${displayName}WillDismiss`,
+ [displayName]
+ );
+ const willPresentEventName = useMemo(
+ () => `on${displayName}WillPresent`,
+ [displayName]
+ );
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ if (isOpen && component && containerElRef.current) {
+ if (React.isValidElement(component)) {
+ ReactDOM.render(component, containerElRef.current);
+ } else {
+ ReactDOM.render(React.createElement(component as React.ComponentClass, componentProps), containerElRef.current);
+ }
+ }
+ }, [component, containerElRef.current, isOpen, componentProps]);
+
+ const present = async (options: OptionsType & HookOverlayOptions) => {
+ if (overlayRef.current) {
+ return;
+ }
+
+ const {
+ onDidDismiss,
+ onWillDismiss,
+ onDidPresent,
+ onWillPresent,
+ ...rest
+ } = options;
+
+ if (typeof document !== 'undefined') {
+ containerElRef.current = document.createElement('div');
+ }
+
+ overlayRef.current = await controller.create({
+ ...(rest as any),
+ component: containerElRef.current
+ });
+
+ attachProps(overlayRef.current, {
+ [didDismissEventName]: handleDismiss,
+ [didPresentEventName]: (e: CustomEvent) =>
+ onDidPresent && onDidPresent(e),
+ [willDismissEventName]: (e: CustomEvent) =>
+ onWillDismiss && onWillDismiss(e),
+ [willPresentEventName]: (e: CustomEvent) =>
+ onWillPresent && onWillPresent(e),
+ });
+
+ overlayRef.current.present();
+
+ setIsOpen(true);
+
+ function handleDismiss(event: CustomEvent>) {
+ if (onDidDismiss) {
+ onDidDismiss(event);
+ }
+ overlayRef.current = undefined;
+ containerElRef.current = undefined;
+ setIsOpen(false);
+ }
+ };
+
+ const dismiss = async () => {
+ overlayRef.current && await overlayRef.current.dismiss();
+ overlayRef.current = undefined;
+ containerElRef.current = undefined;
+ };
+
+ return {
+ present,
+ dismiss,
+ };
+}