Skip to Content
API Reference

API Reference

export type OverlayResolved<ResolvedValue> = { status: 'resolved' value: ResolvedValue } export type OverlayDismissed<DismissedReason> = { status: 'dismissed' reason?: 'unmount' | DismissedReason } export type OverlayOutcome<ResolvedValue, DismissedReason> = | OverlayDismissed<DismissedReason> | OverlayResolved<ResolvedValue> function useAsyncFlow<ResolvedValue = unknown, DismissedReason = unknown>(options?: { /** * The element to restore focus to after the `open()` is resolved or dismissed. * * @default 'previous' */ restoreFocus?: 'previous' | (() => HTMLElement | null) | HTMLElement | { selector: string } /** * Whether to restore focus to the element that triggered the `open()` when the `open()` is resolved by `resolve()`. * * @default true */ restoreFocusOnResolved?: boolean /** * Whether to restore focus to the element that triggered the `open()` when the `open()` is dismissed by `dismiss()`. * * @default true */ restoreFocusOnDismissed?: boolean /** * @default true */ dismissOnUnmount?: boolean }): { isOpen: boolean open: ( event?: { currentTarget: EventTarget }, options?: { restoreFocus?: 'previous' | (() => HTMLElement | null) | HTMLElement | { selector: string } restoreFocusOnResolved?: boolean restoreFocusOnDismissed?: boolean } ) => Promise<OverlayOutcome<ResolvedValue, DismissedReason>> resolve: (value: ResolvedValue) => void dismiss: (reason?: DismissedReason) => void }

Returns

  • isOpen: 모달의 열림 상태
  • open: 모달을 열고 Promise를 반환
  • resolve: 모달을 확정 (값과 함께)
  • dismiss: 모달을 취소 (이유와 함께)

resolve를 호출하면, open()의 Promise가 { status: 'resolved', value: ResolvedValue }로 resolve됩니다.

dismiss를 호출하면, open()의 Promise가 { status: 'dismissed', reason: DismissedReason }로 resolve됩니다.

이 두가지를 조합하여 모달이 닫히는 다양한 조건을 체계적으로 처리할 수 있습니다.

open() 함수의 options 파라미터

open() 함수의 두 번째 파라미터로 RestoreFocusOptions를 전달할 수 있습니다. 이 옵션들은 해당 호출에만 적용되며, 훅 레벨에서 설정한 기본 옵션보다 우선순위가 높습니다.

const overlay = useAsyncFlow({ restoreFocusOnResolved: true, // 훅 레벨 기본값 restoreFocusOnDismissed: true, }) // 이 호출에서만 dismiss 시 포커스 복구 비활성화 const result = await overlay.open(event, { restoreFocusOnDismissed: false, // 이 옵션이 훅 레벨 설정을 덮어씀 })

사용 가능한 옵션:

  • restoreFocus: 포커스를 복구할 대상 요소 지정
  • restoreFocusOnResolved: resolve 시 포커스 복구 여부
  • restoreFocusOnDismissed: dismiss 시 포커스 복구 여부

이를 통해 각각의 모달 호출마다 서로 다른 포커스 복구 전략을 적용할 수 있습니다.

Focus behavior with open(event)

  • open()event 객체를 전달하면, 모달이 resolve 또는 dismiss되는 시점에 event.currentTarget으로 포커스를 옮깁니다. 이는 모달이 닫힐 때 모달을 연 버튼(트리거)으로 포커스를 자연스럽게 복귀시키기 위함입니다.
  • 이 동작은 restoreFocus 옵션이 기본값인 'previous'일 때 활성화됩니다. 다른 방식으로 포커스를 제어하고 싶다면 restoreFocus에 직접 타겟 요소, 셀렉터, 또는 함수를 전달하세요.

Passing events to open()

open(event)는 React 이벤트 또는 { currentTarget: HTMLElement } 형태의 객체를 받을 수 있습니다. 내부에서는 event.currentTarget만 즉시 읽어 트리거 요소로 저장합니다.

// 1) 일반적인 사용: 원본 이벤트를 그대로 전달 button.onClick = async (e) => { const r = await overlay.open(e) } // 2) 같은 트리거로 연속 오픈: currentTarget을 먼저 꺼내어 전달 button.onClick = async (e) => { const target = e.currentTarget as HTMLElement const r1 = await overlay1.open({ currentTarget: target, restoreFocusOnResolved: false }) if (r1.status !== 'resolved') { return } const r2 = await overlay2.open({ currentTarget: target }) }
Note

React 19에서는 예전의 이벤트 풀링이 제거되어 e.persist()는 필요하지도, 의미도 없습니다. 비동기 흐름에서 이벤트 전체를 보관할 필요 없이, e.currentTarget만 즉시 꺼내 로컬 변수로 전달하는 방식을 권장합니다.

Options

  • restoreFocus (default: 'previous')

    • 모달을 닫을 때 포커스를 어디로 되돌릴지 제어합니다.
    • 'previous' | HTMLElement | () => HTMLElement | { selector: string }
    • 권장값은 'previous': open(event) 호출 당시의 버튼 같은 트리거 요소로 자연스럽게 복귀합니다.
  • restoreFocusOnResolved / restoreFocusOnDismissed (default: true / true)

    • 결과 종류에 따라 포커스 복귀 여부를 개별 제어합니다.
    • 예: 사용자가 취소(dismiss)했을 때는 포커스를 굳이 되돌리지 않게 하려면 restoreFocusOnDismissed: false.
  • dismissOnUnmount (default: true)

    • 컴포넌트가 언마운트되거나 조건부 렌더링으로 모달 루트가 사라질 때, 대기 중인 Promise가 영원히 걸리지 않도록 자동으로 { status: 'dismissed', reason: 'unmount' }로 정리합니다.
    • 라우트 전환/탭 이동/조건부 분기 해제 같은 상황에서 메모리·로직 누수를 방지합니다.

Focus control recipes

기본: 트리거로 포커스 복귀

const overlay = useAsyncFlow<boolean, 'close'>(); <button onClick={async (e) => { const r = await overlay.open(e); // 닫힌 뒤 트리거 버튼으로 포커스 복귀 }}> Open </button> <Modal isOpen={overlay.isOpen} onClose={() => overlay.dismiss('close')} />

특정 요소로 복귀: selector

const overlay = useAsyncFlow({ restoreFocus: { selector: '#search-input' } })

동적으로 계산: 함수

const overlay = useAsyncFlow({ restoreFocus: () => document.querySelector('[data-focus="ok"]') as HTMLElement, })

결과별로 포커스 복귀 제어

// 확인(Resolved)일 때만 포커스 복귀, 취소(Dismissed) 시에는 복귀하지 않음 const overlay = useAsyncFlow({ restoreFocusOnResolved: true, restoreFocusOnDismissed: false, })

포커스 복귀를 전부 비활성화

const overlay = useAsyncFlow({ restoreFocusOnResolved: false, restoreFocusOnDismissed: false, })

호출별로 다른 포커스 복구 전략 적용

const overlay = useAsyncFlow({ restoreFocusOnResolved: true, restoreFocusOnDismissed: true, }) // 일반적인 경우: 기본 설정 사용 const result1 = await overlay.open(event) // 특정 호출에서만 포커스 복구 대상 변경 const result2 = await overlay.open(event, { restoreFocus: { selector: '#custom-target' }, }) // 특정 호출에서만 dismiss 시 포커스 복구 비활성화 const result3 = await overlay.open(event, { restoreFocusOnDismissed: false, })
Note

'previous'open(event)에 전달된 event.currentTarget을 사용합니다. event를 전달하지 않았거나 해당 요소가 더 이상 존재하지 않으면 포커스 복귀는 무시됩니다.


Dismissing on unmount

언마운트 시 자동 정리는 실서비스에서 매우 중요합니다. 라우트 전환, 조건부 렌더링 해제, 탭 닫힘 등에서 미처 닫지 못한 오버레이의 Promise가 남지 않도록 합니다.

const overlay = useAsyncFlow<{ ok: boolean }, 'close'>({ dismissOnUnmount: true, // 기본값이 true이므로 생략 가능 })

자동 정리에 의해 반환되는 reason'unmount'입니다.

라우트 전환 등 별도의 사유를 구분하고 싶다면 언마운트 전에 명시적으로 dismiss('route_change')를 호출하는 것을 권장합니다.

function Page() { const overlay = useAsyncFlow<{ ok: boolean }, 'route_change'>() useEffect(() => { const off = router.events.on('routeChangeStart', () => { if (overlay.isOpen) overlay.dismiss('route_change' as const) }) return () => off() }, [overlay.isOpen]) // ... }
Last updated on