1- import React , { useEffect , useRef } from 'react' ;
1+ import React , { useContext , useEffect , useMemo , useRef } from 'react' ;
22import { PropsWithChildren , useCallback , useState } from 'react' ;
33import DialogContext , { dialogContextState } from '../DialogContext' ;
44import { DialogComponent } from '../types' ;
@@ -16,6 +16,7 @@ const DialogProvider = ({
1616 defaultUnmountDelayInMs,
1717 children,
1818} : DialogProviderProps ) => {
19+ // This ref tracks timers for unmount dialogs after they're closed
1920 const unmountDelayTimeoutRefs = useRef < { [ key : string ] : any } > ( { } ) ;
2021
2122 const [ dialogState , setDialogState ] = useState < dialogsStateData > ( { } ) ;
@@ -24,39 +25,36 @@ const DialogProvider = ({
2425 /**
2526 * Force closes the dialog with the given id.
2627 */
27- const hide = useCallback (
28- ( id : string ) => {
29- setDialogState ( ( state ) => {
30- if ( ! state [ id ] ) return state ;
28+ const hide = useCallback ( ( id : string ) => {
29+ setDialogState ( ( state ) => {
30+ if ( ! state [ id ] ) return state ;
3131
32- if ( ! state [ id ] . open ) return state ; // don't do anything if the dialog is already closed
32+ if ( ! state [ id ] . open ) return state ; // don't do anything if the dialog is already closed
3333
34- state [ id ] . resolve ?.( undefined ) ;
34+ state [ id ] . resolve ?.( undefined ) ;
3535
36- if ( state [ id ] . unmountDelay ) {
37- if ( unmountDelayTimeoutRefs . current [ id ] !== undefined ) {
38- clearTimeout ( unmountDelayTimeoutRefs . current [ id ] ) ;
39- }
36+ if ( state [ id ] . unmountDelay ) {
37+ if ( unmountDelayTimeoutRefs . current [ id ] !== undefined ) {
38+ clearTimeout ( unmountDelayTimeoutRefs . current [ id ] ) ;
39+ }
4040
41- // start the delay
42- unmountDelayTimeoutRefs . current [ id ] = setTimeout ( ( ) => {
43- setDialogState ( ( state ) => removeKey ( state , id ) ) ;
44- } , state [ id ] . unmountDelay ) ;
41+ // start the delay
42+ unmountDelayTimeoutRefs . current [ id ] = setTimeout ( ( ) => {
43+ setDialogState ( ( state ) => removeKey ( state , id ) ) ;
44+ } , state [ id ] . unmountDelay ) ;
4545
46- return {
47- ...state ,
48- [ id ] : {
49- open : false ,
50- dialog : state [ id ] . dialog ,
51- data : state [ id ] . data ,
52- } ,
53- } ;
54- }
55- return removeKey ( state , id ) ;
56- } ) ;
57- } ,
58- [ setDialogState ] ,
59- ) ;
46+ return {
47+ ...state ,
48+ [ id ] : {
49+ open : false ,
50+ dialog : state [ id ] . dialog ,
51+ data : state [ id ] . data ,
52+ } ,
53+ } ;
54+ }
55+ return removeKey ( state , id ) ;
56+ } ) ;
57+ } , [ ] ) ;
6058
6159 const show = useCallback (
6260 (
@@ -66,9 +64,6 @@ const DialogProvider = ({
6664 data : unknown ,
6765 unmountDelay ?: number ,
6866 ) : Promise < unknown > => {
69- // if already open, do nothing
70- if ( dialogState [ id ] ?. open ) return Promise . resolve ( ) ;
71-
7267 return new Promise ( ( resolve ) => {
7368 if ( unmountDelayTimeoutRefs . current [ id ] !== undefined ) {
7469 clearTimeout ( unmountDelayTimeoutRefs . current [ id ] ) ;
@@ -79,71 +74,70 @@ const DialogProvider = ({
7974 hide ( id ) ;
8075 } ;
8176
82- setDialogState ( ( state ) => ( {
83- ...state ,
84- [ id ] : {
85- dialog : dialog ,
86- open : true ,
87- hash,
88- data,
89- resolve : resolveFn ,
90- unmountDelay : unmountDelay ?? defaultUnmountDelayInMs ,
91- } ,
92- } ) ) ;
93- } ) ;
94- } ,
95- [ setDialogState , hide , defaultUnmountDelayInMs ] ,
96- ) ;
77+ setDialogState ( ( state ) => {
78+ if ( state [ id ] ?. open ) {
79+ resolve ( undefined ) ;
80+ return state ;
81+ }
9782
98- /**
99- * Updates the data of the given dialog
100- */
101- const updateData = useCallback (
102- ( id : string , data : unknown ) : void => {
103- setDialogState ( ( state ) => {
104- if ( state [ id ] ) {
10583 return {
10684 ...state ,
107- [ id ] : { ...state [ id ] , data } ,
85+ [ id ] : {
86+ dialog : dialog ,
87+ open : true ,
88+ hash,
89+ data,
90+ resolve : resolveFn ,
91+ unmountDelay : unmountDelay ?? defaultUnmountDelayInMs ,
92+ } ,
10893 } ;
109- }
110- return state ;
94+ } ) ;
11195 } ) ;
11296 } ,
113- [ setDialogState ] ,
97+ [ hide , defaultUnmountDelayInMs ] ,
11498 ) ;
11599
116- const dialogComponents = useRenderDialogs ( dialogState ) ;
117-
118- if (
119- process . env . NODE_ENV !== 'production' &&
120- ! usingOutlet &&
121- dialogComponents . length > 0
122- ) {
123- console . warn (
124- 'Rendering a dialog without a <DialogOutlet/>. Please include a <DialogOutlet/> as a child of <DialogProvider/> to ensure dialogs are rendered within the correct contexts - this will be required in the next major version of react-dialog-async. See https://react-dialog-async.a16n.dev/API/dialog-outlet for more details. This warning is only present in development' ,
125- ) ;
126- }
100+ /**
101+ * Updates the data of the given dialog
102+ */
103+ const updateData = useCallback ( ( id : string , data : unknown ) : void => {
104+ setDialogState ( ( state ) => {
105+ if ( state [ id ] ) {
106+ return {
107+ ...state ,
108+ [ id ] : { ...state [ id ] , data } ,
109+ } ;
110+ }
111+ return state ;
112+ } ) ;
113+ } , [ ] ) ;
127114
128115 useEffect ( ( ) => {
129116 return ( ) => {
130117 Object . values ( unmountDelayTimeoutRefs . current ) . forEach ( clearTimeout ) ;
131118 } ;
132119 } , [ ] ) ;
133120
134- const ctx : dialogContextState = {
135- show,
136- hide,
137- updateData,
138- } ;
121+ /**
122+ * To prevent unnecessary re-renders, be careful to ensure that this state has
123+ * no transitive dependencies to `dialogState`
124+ */
125+ const ctx : dialogContextState = useMemo (
126+ ( ) => ( {
127+ show,
128+ hide,
129+ updateData,
130+ } ) ,
131+ [ show , hide , updateData ] ,
132+ ) ;
139133
140134 return (
141135 < DialogStateContext . Provider
142136 value = { { dialogs : dialogState , setIsUsingOutlet : setUsingOutlet } }
143137 >
144138 < DialogContext . Provider value = { ctx } >
145139 { children }
146- { ! usingOutlet && dialogComponents }
140+ { ! usingOutlet && < InternalDialogOutlet /> }
147141 </ DialogContext . Provider >
148142 </ DialogStateContext . Provider >
149143 ) ;
@@ -158,3 +152,23 @@ function removeKey<
158152 const { [ key ] : _ , ...rest } = data ;
159153 return rest ;
160154}
155+
156+ const InternalDialogOutlet = ( ) => {
157+ const dialogState = useContext ( DialogStateContext ) ;
158+
159+ if ( ! dialogState ) {
160+ throw new Error (
161+ 'Dialog context not found. You likely forgot to wrap your app in a <DialogProvider/> (https://react-dialog-async.a16n.dev/installation)' ,
162+ ) ;
163+ }
164+
165+ const dialogComponents = useRenderDialogs ( dialogState . dialogs ) ;
166+
167+ if ( process . env . NODE_ENV !== 'production' && dialogComponents . length > 0 ) {
168+ console . warn (
169+ 'Rendering a dialog without a <DialogOutlet/>. Please include a <DialogOutlet/> as a child of <DialogProvider/> to ensure dialogs are rendered within the correct contexts - this will be required in the next major version of react-dialog-async. See https://react-dialog-async.a16n.dev/API/dialog-outlet for more details. This warning is only present in development' ,
170+ ) ;
171+ }
172+
173+ return < > { dialogComponents } </ > ;
174+ } ;
0 commit comments