@inertiaui/modal-react 2.0.0-beta.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import { default as Modal } from './Modal';
8
8
  import { default as ModalLink } from './ModalLink';
9
9
  import { default as WhenVisible } from './WhenVisible';
10
10
  import * as dialogUtils from '@inertiaui/vanilla';
11
- export type { Modal as ModalInstance, ModalConfig, ModalResponseData, ModalStackContextValue, VisitOptions, ReloadOptions, EventCallback, ComponentResolver, PageProps, ModalRootProps, ModalRendererProps, LocalModal, PrefetchOption, PrefetchOptions, } from './types';
11
+ export type { Modal as ModalInstance, ModalConfig, ModalResponseData, ModalStackContextValue, VisitOptions, ReloadOptions, EventCallback, ComponentResolver, HttpMethod, PageProps, ModalRootProps, ModalRendererProps, LocalModal, PrefetchOption, PrefetchOptions, } from './types';
12
12
  export type { ModalTypeConfig } from './config';
13
13
  export type { CleanupFunction, FocusTrapOptions, EscapeKeyOptions } from '@inertiaui/vanilla';
14
14
  declare const setPageLayout: <T extends {
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { AxiosResponse } from 'axios';
2
1
  import { ComponentType, ReactNode } from 'react';
3
- import { RequestPayload } from '@inertiajs/core';
2
+ import { RequestPayload, HttpResponse, Method } from '@inertiajs/core';
3
+ import { ModalTypeConfig } from './config';
4
+ export type HttpMethod = Method;
4
5
  export interface ModalResponseData {
5
6
  id?: string;
6
7
  component: string;
@@ -12,22 +13,22 @@ export interface ModalResponseData {
12
13
  };
13
14
  baseUrl?: string;
14
15
  }
15
- export interface ModalConfig {
16
- [key: string]: unknown;
17
- }
16
+ export type ModalConfig = Partial<ModalTypeConfig & {
17
+ slideover: boolean;
18
+ }>;
18
19
  export interface ReloadOptions {
19
20
  only?: string[];
20
21
  except?: string[];
21
- method?: string;
22
+ method?: HttpMethod;
22
23
  data?: Record<string, unknown>;
23
24
  headers?: Record<string, string>;
24
25
  onStart?: () => void;
25
- onSuccess?: (response: AxiosResponse) => void;
26
+ onSuccess?: (response: HttpResponse) => void;
26
27
  onError?: (error: unknown) => void;
27
28
  onFinish?: () => void;
28
29
  }
29
30
  export interface VisitOptions {
30
- method?: string;
31
+ method?: HttpMethod;
31
32
  data?: RequestPayload;
32
33
  headers?: Record<string, string>;
33
34
  config?: ModalConfig;
@@ -36,14 +37,14 @@ export interface VisitOptions {
36
37
  queryStringArrayFormat?: 'brackets' | 'indices';
37
38
  navigate?: boolean;
38
39
  onStart?: () => void;
39
- onSuccess?: (response?: AxiosResponse) => void;
40
+ onSuccess?: (response?: HttpResponse) => void;
40
41
  onError?: (...args: unknown[]) => void;
41
42
  listeners?: Record<string, (...args: unknown[]) => void>;
42
43
  props?: Record<string, unknown>;
43
44
  }
44
45
  export type PrefetchOption = boolean | 'hover' | 'click' | 'mount' | Array<'hover' | 'click' | 'mount'>;
45
46
  export interface PrefetchOptions {
46
- method?: string;
47
+ method?: HttpMethod;
47
48
  data?: RequestPayload;
48
49
  headers?: Record<string, string>;
49
50
  queryStringArrayFormat?: 'brackets' | 'indices';
@@ -92,7 +93,7 @@ export interface ModalStackContextValue {
92
93
  length: () => number;
93
94
  closeAll: (force?: boolean) => void;
94
95
  reset: () => void;
95
- visit: (href: string, method: string, payload?: RequestPayload, headers?: Record<string, string>, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null, queryStringArrayFormat?: 'brackets' | 'indices', useBrowserHistory?: boolean, onStart?: (() => void) | null, onSuccess?: ((response?: AxiosResponse) => void) | null, onError?: ((...args: unknown[]) => void) | null) => Promise<Modal>;
96
+ visit: (href: string, method: HttpMethod, payload?: RequestPayload, headers?: Record<string, string>, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null, queryStringArrayFormat?: 'brackets' | 'indices', useBrowserHistory?: boolean, onStart?: (() => void) | null, onSuccess?: ((response?: HttpResponse) => void) | null, onError?: ((...args: unknown[]) => void) | null) => Promise<Modal>;
96
97
  visitModal: (url: string, options?: VisitOptions) => Promise<Modal>;
97
98
  registerLocalModal: (name: string, callback: (modal: Modal) => void) => void;
98
99
  removeLocalModal: (name: string) => void;
@@ -101,7 +102,6 @@ export interface PageProps {
101
102
  initialPage?: {
102
103
  version?: string;
103
104
  };
104
- resolveComponent?: ComponentResolver;
105
105
  }
106
106
  export interface ModalRootProps {
107
107
  children?: ReactNode;
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@inertiaui/modal-react",
3
3
  "author": "Pascal Baljet <pascal@protone.media>",
4
- "version": "2.0.0-beta.1",
4
+ "version": "3.0.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/inertiaui/modal.git",
10
+ "directory": "react"
11
+ },
7
12
  "type": "module",
8
13
  "files": [
9
14
  "dist",
@@ -30,8 +35,8 @@
30
35
  },
31
36
  "devDependencies": {
32
37
  "@heroicons/react": "^2.1.4",
33
- "@inertiajs/react": "^2.3.15",
34
- "@inertiaui/vanilla": "^0.2.0",
38
+ "@inertiajs/react": "^3.0.0",
39
+ "@inertiaui/vanilla": "^0.3.0",
35
40
  "@testing-library/react": "^16.0.0",
36
41
  "@types/react": "^19.0.0",
37
42
  "@types/react-dom": "^19.0.0",
@@ -40,7 +45,6 @@
40
45
  "@vitejs/plugin-react": "^4.3.1",
41
46
  "@vitest/coverage-v8": "^4.0.0",
42
47
  "@vitest/ui": "^4.0.0",
43
- "axios": "^1.6.0",
44
48
  "clsx": "^2.1.1",
45
49
  "eslint": "^9.0.0",
46
50
  "happy-dom": "^20.0.0",
@@ -53,12 +57,11 @@
53
57
  "vitest": "^4.0.0"
54
58
  },
55
59
  "dependencies": {
56
- "@inertiaui/vanilla": "^0.2.0"
60
+ "@inertiaui/vanilla": "^0.3.0"
57
61
  },
58
62
  "peerDependencies": {
59
- "@inertiajs/react": "^2.3.15",
60
- "axios": "^1.6.0",
63
+ "@inertiajs/react": "^3.0.0",
61
64
  "react": "^19.0.0",
62
65
  "react-dom": "^19.0.0"
63
66
  }
64
- }
67
+ }
package/src/ModalLink.tsx CHANGED
@@ -2,12 +2,12 @@ import { useCallback, useState, useEffect, useMemo, useRef, ReactNode, ElementTy
2
2
  import { useModalStack, modalPropNames, prefetch as prefetchModal } from './ModalRoot'
3
3
  import { only, rejectNullValues, isStandardDomEvent } from './helpers'
4
4
  import { getConfig } from './config'
5
- import type { Modal, PrefetchOption } from './types'
5
+ import type { Modal, PrefetchOption, HttpMethod } from './types'
6
6
  import type { RequestPayload } from '@inertiajs/core'
7
7
 
8
8
  interface ModalLinkProps {
9
9
  href: string
10
- method?: string
10
+ method?: HttpMethod
11
11
  data?: RequestPayload
12
12
  as?: ElementType
13
13
  headers?: Record<string, string>
package/src/ModalRoot.tsx CHANGED
@@ -1,8 +1,8 @@
1
- import { createElement, useEffect, useState, useRef, useReducer, ReactNode, ComponentType } from 'react'
2
- import { default as Axios, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
- import { except, kebabCase, generateId, sameUrlPath } from './helpers'
4
- import { router, usePage, progress } from '@inertiajs/react'
5
- import { mergeDataIntoQueryString, type RequestPayload } from '@inertiajs/core'
1
+ import { createElement, useEffect, useLayoutEffect, useState, useRef, useReducer, ReactNode, ComponentType } from 'react'
2
+ import { except, kebabCase, generateId, sameUrlPath, parseResponseData } from './helpers'
3
+ import { ResponseCache } from './cache'
4
+ import { router, usePage, progress, http } from '@inertiajs/react'
5
+ import { mergeDataIntoQueryString, type RequestPayload, type HttpResponse, type HttpRequestConfig } from '@inertiajs/core'
6
6
  import { createContext, useContext } from 'react'
7
7
  import ModalRenderer from './ModalRenderer'
8
8
  import { getConfig } from './config'
@@ -14,7 +14,7 @@ import type {
14
14
  VisitOptions,
15
15
  ReloadOptions,
16
16
  EventCallback,
17
- ComponentResolver,
17
+ HttpMethod,
18
18
  PageProps,
19
19
  ModalRootProps,
20
20
  LocalModal,
@@ -24,75 +24,38 @@ import type {
24
24
  const ModalStackContext = createContext<ModalStackContextValue | null>(null)
25
25
  ModalStackContext.displayName = 'ModalStackContext'
26
26
 
27
- let pageVersion: string | null = null
28
- let resolveComponent: ComponentResolver | null = null
29
27
  let baseUrl: string | null = null
28
+ let currentPageVersion: string | null = null
30
29
 
31
30
  // Track the URL we're closing to (prevents navigate handler from re-setting baseUrl)
32
31
  // Only suppresses if navigate event URL matches this URL
33
32
  let closingToBaseUrlTarget: string | null = null
34
33
 
35
34
  // Prefetch cache (#146)
36
- interface PrefetchCacheEntry {
37
- response: AxiosResponse
38
- timestamp: number
39
- expiresAt: number
40
- }
41
-
42
- const prefetchCache = new Map<string, PrefetchCacheEntry>()
43
- const prefetchInFlight = new Map<string, Promise<AxiosResponse>>()
44
-
45
- function getPrefetchCacheKey(url: string, method: string, data: RequestPayload): string {
46
- return `${method}:${url}:${JSON.stringify(data)}`
47
- }
48
-
49
- function getCachedResponse(url: string, method: string, data: RequestPayload): AxiosResponse | null {
50
- const key = getPrefetchCacheKey(url, method, data)
51
- const cached = prefetchCache.get(key)
52
-
53
- if (!cached) {
54
- return null
55
- }
56
-
57
- if (Date.now() > cached.expiresAt) {
58
- prefetchCache.delete(key)
59
- return null
60
- }
61
-
62
- return cached.response
63
- }
64
-
65
- function setCachedResponse(url: string, method: string, data: RequestPayload, response: AxiosResponse, cacheFor: number): void {
66
- const key = getPrefetchCacheKey(url, method, data)
67
- prefetchCache.set(key, {
68
- response,
69
- timestamp: Date.now(),
70
- expiresAt: Date.now() + cacheFor,
71
- })
72
- }
35
+ const prefetchCache = new ResponseCache<HttpResponse>()
73
36
 
74
37
  export function prefetch(href: string, options: PrefetchOptions = {}): Promise<void> {
75
38
  if (href.startsWith('#')) {
76
39
  return Promise.resolve()
77
40
  }
78
41
 
79
- const method = (options.method ?? 'get').toLowerCase()
42
+ const method = options.method ?? 'get'
80
43
  const data = options.data ?? ({} as RequestPayload)
81
44
  const headers = options.headers ?? {}
82
45
  const queryStringArrayFormat = options.queryStringArrayFormat ?? 'brackets'
83
46
  const cacheFor = options.cacheFor ?? 30000
84
47
 
85
- const [url, mergedData] = mergeDataIntoQueryString(method as 'get' | 'post' | 'put' | 'patch' | 'delete', href || '', data, queryStringArrayFormat)
48
+ const [url, mergedData] = mergeDataIntoQueryString(method, href || '', data, queryStringArrayFormat)
49
+
50
+ const cacheKey = ResponseCache.key(method, url, mergedData)
86
51
 
87
52
  // Check if already cached
88
- const cached = getCachedResponse(url, method, mergedData)
89
- if (cached) {
53
+ if (prefetchCache.get(cacheKey)) {
90
54
  return Promise.resolve()
91
55
  }
92
56
 
93
57
  // Check if already in flight
94
- const cacheKey = getPrefetchCacheKey(url, method, mergedData)
95
- const inFlight = prefetchInFlight.get(cacheKey)
58
+ const inFlight = prefetchCache.getInFlight(cacheKey)
96
59
  if (inFlight) {
97
60
  return inFlight.then(() => {})
98
61
  }
@@ -104,27 +67,29 @@ export function prefetch(href: string, options: PrefetchOptions = {}): Promise<v
104
67
  Accept: 'text/html, application/xhtml+xml',
105
68
  'X-Requested-With': 'XMLHttpRequest',
106
69
  'X-Inertia': 'true',
107
- 'X-Inertia-Version': pageVersion ?? '',
70
+ 'X-Inertia-Version': currentPageVersion ?? '',
108
71
  'X-InertiaUI-Modal': generateId(),
109
72
  'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
110
73
  }
111
74
 
112
- const request = Axios({
113
- url,
114
- method,
115
- data: mergedData,
116
- headers: requestHeaders,
117
- })
75
+ const request = http
76
+ .getClient()
77
+ .request({
78
+ url,
79
+ method,
80
+ data: mergedData,
81
+ headers: requestHeaders,
82
+ })
118
83
  .then((response) => {
119
- setCachedResponse(url, method, mergedData, response, cacheFor)
84
+ prefetchCache.set(cacheKey, response, cacheFor)
120
85
  options.onPrefetched?.()
121
86
  return response
122
87
  })
123
88
  .finally(() => {
124
- prefetchInFlight.delete(cacheKey)
89
+ prefetchCache.deleteInFlight(cacheKey)
125
90
  })
126
91
 
127
- prefetchInFlight.set(cacheKey, request)
92
+ prefetchCache.setInFlight(cacheKey, request)
128
93
 
129
94
  return request.then(() => {})
130
95
  }
@@ -365,29 +330,30 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
365
330
  return
366
331
  }
367
332
 
368
- const method = (options.method ?? 'get').toLowerCase()
333
+ const method = options.method ?? 'get'
369
334
  const data = options.data ?? {}
370
335
 
371
336
  options.onStart?.()
372
337
 
373
- Axios({
374
- url: this.response.url,
375
- method,
376
- data: method === 'get' ? {} : data,
377
- params: method === 'get' ? data : {},
378
- headers: {
379
- ...(options.headers ?? {}),
380
- Accept: 'text/html, application/xhtml+xml',
381
- 'X-Inertia': 'true',
382
- 'X-Inertia-Partial-Component': this.response.component,
383
- 'X-Inertia-Version': this.response.version ?? '',
384
- 'X-Inertia-Partial-Data': keys.join(','),
385
- 'X-InertiaUI-Modal': generateId(),
386
- 'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
387
- },
388
- })
338
+ http.getClient()
339
+ .request({
340
+ url: this.response.url,
341
+ method,
342
+ data: method === 'get' ? undefined : data,
343
+ params: method === 'get' ? data : undefined,
344
+ headers: {
345
+ ...(options.headers ?? {}),
346
+ Accept: 'text/html, application/xhtml+xml',
347
+ 'X-Inertia': 'true',
348
+ 'X-Inertia-Partial-Component': this.response.component,
349
+ 'X-Inertia-Version': this.response.version ?? '',
350
+ 'X-Inertia-Partial-Data': keys.join(','),
351
+ 'X-InertiaUI-Modal': generateId(),
352
+ 'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
353
+ },
354
+ })
389
355
  .then((response) => {
390
- this.updateProps(response.data.props)
356
+ this.updateProps((parseResponseData(response.data) as ModalResponseData).props)
391
357
 
392
358
  options.onSuccess?.(response)
393
359
  })
@@ -420,10 +386,6 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
420
386
  onClose: (() => void) | null = null,
421
387
  onAfterLeave: (() => void) | null = null,
422
388
  ): Promise<Modal> => {
423
- if (!resolveComponent) {
424
- return Promise.reject(new Error('resolveComponent not set'))
425
- }
426
-
427
389
  if (!isValidModalResponse(responseData)) {
428
390
  return Promise.reject(
429
391
  new Error(
@@ -433,8 +395,8 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
433
395
  )
434
396
  }
435
397
 
436
- return resolveComponent(responseData.component).then((component) =>
437
- push(component, responseData, config, onClose, onAfterLeave),
398
+ return router.resolveComponent(responseData.component).then((component) =>
399
+ push(component as ComponentType, responseData, config, onClose, onAfterLeave),
438
400
  )
439
401
  }
440
402
 
@@ -537,7 +499,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
537
499
 
538
500
  const visit = (
539
501
  href: string,
540
- method: string,
502
+ method: HttpMethod,
541
503
  payload: RequestPayload = {},
542
504
  headers: Record<string, string> = {},
543
505
  config: ModalConfig = {},
@@ -546,7 +508,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
546
508
  queryStringArrayFormat: 'brackets' | 'indices' = 'brackets',
547
509
  useBrowserHistory = false,
548
510
  onStart: (() => void) | null = null,
549
- onSuccess: ((response?: AxiosResponse) => void) | null = null,
511
+ onSuccess: ((response?: HttpResponse) => void) | null = null,
550
512
  onError: ((...args: unknown[]) => void) | null = null,
551
513
  props: Record<string, unknown> | null = null,
552
514
  ): Promise<Modal> => {
@@ -558,15 +520,16 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
558
520
  return
559
521
  }
560
522
 
561
- const [url, data] = mergeDataIntoQueryString(method as 'get' | 'post' | 'put' | 'patch' | 'delete', href || '', payload, queryStringArrayFormat)
523
+ const [url, data] = mergeDataIntoQueryString(method, href || '', payload, queryStringArrayFormat)
562
524
 
563
525
  // Check for cached prefetch response (#146)
564
- const cachedResponse = getCachedResponse(url, method, data)
526
+ const cachedResponse = prefetchCache.get(ResponseCache.key(method, url, data))
565
527
  if (cachedResponse) {
528
+ const cachedData = parseResponseData(cachedResponse.data) as ModalResponseData
566
529
  onSuccess?.(cachedResponse)
567
- pushFromResponseData(cachedResponse.data, config, onClose, onAfterLeave)
530
+ pushFromResponseData(cachedData, config, onClose, onAfterLeave)
568
531
  .then((modal) => {
569
- updateBrowserUrl(cachedResponse.data.url, useBrowserHistory, cachedResponse.data)
532
+ updateBrowserUrl(cachedData.url, useBrowserHistory, cachedData)
570
533
  resolve(modal)
571
534
  })
572
535
  .catch(reject)
@@ -582,7 +545,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
582
545
  Accept: 'text/html, application/xhtml+xml',
583
546
  'X-Requested-With': 'XMLHttpRequest',
584
547
  'X-Inertia': 'true',
585
- 'X-Inertia-Version': pageVersion ?? '',
548
+ 'X-Inertia-Version': currentPageVersion ?? '',
586
549
  'X-InertiaUI-Modal': modalId,
587
550
  'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
588
551
  }
@@ -591,17 +554,19 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
591
554
 
592
555
  progress?.start()
593
556
 
594
- Axios({
595
- url,
596
- method,
597
- data,
598
- headers: requestHeaders,
599
- })
557
+ http.getClient()
558
+ .request({
559
+ url,
560
+ method,
561
+ data,
562
+ headers: requestHeaders,
563
+ })
600
564
  .then((response) => {
565
+ const responseData = parseResponseData(response.data) as ModalResponseData
601
566
  onSuccess?.(response)
602
- pushFromResponseData(response.data, config, onClose, onAfterLeave)
567
+ pushFromResponseData(responseData, config, onClose, onAfterLeave)
603
568
  .then((modal) => {
604
- updateBrowserUrl(response.data.url, useBrowserHistory, response.data)
569
+ updateBrowserUrl(responseData.url, useBrowserHistory, responseData)
605
570
  resolve(modal)
606
571
  })
607
572
  .catch(reject)
@@ -671,11 +636,7 @@ export const modalPropNames = ['closeButton', 'closeExplicitly', 'closeOnClickOu
671
636
 
672
637
  export const initFromPageProps = (pageProps: PageProps) => {
673
638
  if (pageProps.initialPage) {
674
- pageVersion = pageProps.initialPage.version ?? null
675
- }
676
-
677
- if (pageProps.resolveComponent) {
678
- resolveComponent = pageProps.resolveComponent
639
+ currentPageVersion = pageProps.initialPage.version ?? null
679
640
  }
680
641
  }
681
642
 
@@ -734,11 +695,30 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
734
695
  const $page = usePage<InertiaUIModalPageProps>()
735
696
  const pendingModalKeysRef = useRef(new Set<string>())
736
697
 
698
+ // Keep module-level pageVersion in sync for use by prefetch/visit functions.
699
+ // Set during render (not useEffect) because prefetch() needs it synchronously on mount.
700
+ currentPageVersion = $page.version ?? null
701
+
737
702
  // Generate a unique key for deduplication (handles case when modal has no id)
738
703
  const getModalKey = (modalData: ModalResponseData) => modalData.id || `${modalData.component}:${modalData.url}`
739
704
 
740
705
  const isNavigatingRef = useRef(false)
741
- const initialModalStillOpenedRef = useRef(!!$page.props?._inertiaui_modal)
706
+
707
+ // Use a ref so the interceptor always reads the latest page props
708
+ const pageRef = useRef($page)
709
+ pageRef.current = $page
710
+
711
+ // Register interceptor in useLayoutEffect (fires during commit, before microtasks).
712
+ // Inertia 3 loads deferred props during page.set() microtasks which fire after commit
713
+ // but before useEffect — useLayoutEffect ensures the interceptor is registered in time.
714
+ useLayoutEffect(() => http.onRequest((config: HttpRequestConfig) => {
715
+ const baseUrlValue = baseUrl ?? pageRef.current.props._inertiaui_modal?.baseUrl ?? null
716
+ if (baseUrlValue) {
717
+ config.headers = config.headers ?? {}
718
+ config.headers['X-InertiaUI-Modal-Base-Url'] = baseUrlValue
719
+ }
720
+ return config
721
+ }), [])
742
722
 
743
723
  useEffect(() => router.on('start', () => (isNavigatingRef.current = true)), [])
744
724
  useEffect(() => router.on('finish', () => (isNavigatingRef.current = false)), [])
@@ -759,7 +739,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
759
739
  closingToBaseUrlTarget = null
760
740
  context?.closeAll(true)
761
741
  baseUrl = null
762
- initialModalStillOpenedRef.current = false
763
742
  return
764
743
  }
765
744
  closingToBaseUrlTarget = null
@@ -769,7 +748,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
769
748
  // No modal data - close any open modals (force close without transition)
770
749
  context?.closeAll(true)
771
750
  baseUrl = null
772
- initialModalStillOpenedRef.current = false
773
751
  return
774
752
  }
775
753
 
@@ -777,7 +755,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
777
755
  if (!sameUrlPath(pageUrl, modalOnBase.url)) {
778
756
  context?.closeAll(true)
779
757
  baseUrl = null
780
- initialModalStillOpenedRef.current = false
781
758
  return
782
759
  }
783
760
 
@@ -809,6 +786,11 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
809
786
  console.error('No base url in modal response data so cannot navigate back')
810
787
  return
811
788
  }
789
+ // Clear baseUrl before navigating so the interceptor doesn't add
790
+ // the modal header to the base page request (deferred props should
791
+ // load without the modal context after closing)
792
+ baseUrl = null
793
+
812
794
  if (!isNavigatingRef.current && typeof window !== 'undefined' && window.location.href !== modalOnBase.baseUrl) {
813
795
  router.visit(modalOnBase.baseUrl, {
814
796
  preserveScroll: true,
@@ -823,23 +805,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
823
805
  [],
824
806
  )
825
807
 
826
- const axiosRequestInterceptor = (config: InternalAxiosRequestConfig) => {
827
- // A Modal is opened on top of a base route, so we need to pass this base route
828
- // so it can redirect back with the back() helper method...
829
- // Only send the header when we have an actual base URL value
830
- const baseUrlValue = baseUrl ?? (initialModalStillOpenedRef.current ? $page.props._inertiaui_modal?.baseUrl : null)
831
- if (baseUrlValue) {
832
- config.headers['X-InertiaUI-Modal-Base-Url'] = baseUrlValue
833
- }
834
-
835
- return config
836
- }
837
-
838
- useEffect(() => {
839
- const interceptorId = Axios.interceptors.request.use(axiosRequestInterceptor)
840
- return () => Axios.interceptors.request.eject(interceptorId)
841
- }, [])
842
-
843
808
  const previousModalRef = useRef<(ModalResponseData & { baseUrl: string }) | undefined>(undefined)
844
809
 
845
810
  useEffect(() => {
@@ -878,3 +843,4 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
878
843
  </>
879
844
  )
880
845
  }
846
+
package/src/cache.ts ADDED
@@ -0,0 +1,64 @@
1
+ interface CacheEntry<T> {
2
+ response: T
3
+ expiresAt: number
4
+ }
5
+
6
+ export class ResponseCache<T> {
7
+ private cache = new Map<string, CacheEntry<T>>()
8
+ private timers = new Map<string, ReturnType<typeof setTimeout>>()
9
+ private inFlight = new Map<string, Promise<T>>()
10
+
11
+ static key(method: string, url: string, data: unknown): string {
12
+ return `${method}:${url}:${JSON.stringify(data)}`
13
+ }
14
+
15
+ get(key: string): T | null {
16
+ const cached = this.cache.get(key)
17
+
18
+ if (!cached) {
19
+ return null
20
+ }
21
+
22
+ if (Date.now() > cached.expiresAt) {
23
+ this.delete(key)
24
+ return null
25
+ }
26
+
27
+ return cached.response
28
+ }
29
+
30
+ set(key: string, response: T, cacheFor: number): void {
31
+ this.delete(key)
32
+
33
+ this.cache.set(key, {
34
+ response,
35
+ expiresAt: Date.now() + cacheFor,
36
+ })
37
+
38
+ if (cacheFor > 0) {
39
+ this.timers.set(key, setTimeout(() => this.delete(key), cacheFor))
40
+ }
41
+ }
42
+
43
+ delete(key: string): void {
44
+ this.cache.delete(key)
45
+
46
+ const timer = this.timers.get(key)
47
+ if (timer) {
48
+ clearTimeout(timer)
49
+ this.timers.delete(key)
50
+ }
51
+ }
52
+
53
+ getInFlight(key: string): Promise<T> | undefined {
54
+ return this.inFlight.get(key)
55
+ }
56
+
57
+ setInFlight(key: string, promise: Promise<T>): void {
58
+ this.inFlight.set(key, promise)
59
+ }
60
+
61
+ deleteInFlight(key: string): void {
62
+ this.inFlight.delete(key)
63
+ }
64
+ }
package/src/helpers.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  // Re-export helper utilities from vanilla
2
2
  export { sameUrlPath, except, only, rejectNullValues, kebabCase, isStandardDomEvent } from '@inertiaui/vanilla'
3
+ export function parseResponseData(data: unknown): unknown {
4
+ return typeof data === 'string' ? JSON.parse(data) : data
5
+ }
3
6
  import { generateId as vanillaGenerateId } from '@inertiaui/vanilla'
4
7
 
5
8
  // Wrap generateId with custom callback support for testing
@@ -20,6 +20,7 @@ export type {
20
20
  ReloadOptions,
21
21
  EventCallback,
22
22
  ComponentResolver,
23
+ HttpMethod,
23
24
  PageProps,
24
25
  ModalRootProps,
25
26
  ModalRendererProps,