react-flow-modal 0.4.1 → 0.6.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.
package/README.md CHANGED
@@ -22,7 +22,9 @@ yarn add react-flow-modal
22
22
  ## Basic Usage
23
23
 
24
24
  ```tsx
25
- import { ModalProvider, useModal, renderModals } from "react-flow-modal";
25
+ import { StrictMode } from 'react';
26
+ import { createRoot } from 'react-dom/client';
27
+ import { ModalProvider, useModal, ModalHost } from "react-flow-modal";
26
28
 
27
29
  function ConfirmModal({
28
30
  onConfirm,
@@ -66,6 +68,7 @@ function App() {
66
68
  const modal = useModal();
67
69
 
68
70
  const onClick = async () => {
71
+ // render modal and await resolve
69
72
  const result = await modal.open("confirm", (resolve) => (
70
73
  <ConfirmModal
71
74
  onConfirm={() => resolve(true)}
@@ -73,24 +76,22 @@ function App() {
73
76
  />
74
77
  ));
75
78
 
79
+ // flow resumed
76
80
  console.log("Result:", result);
77
81
  };
78
82
 
79
83
  return <button onClick={onClick}>Open Confirm Modal</button>;
80
84
  }
81
85
 
82
- function ModalRenderer() {
83
- return renderModals();
84
- }
85
-
86
- export default function Root() {
87
- return (
86
+ createRoot(document.getElementById('root')!).render(
87
+ <StrictMode>
88
88
  <ModalProvider>
89
89
  <App />
90
- <ModalRenderer />
90
+ <ModalHost />
91
91
  </ModalProvider>
92
- );
93
- }
92
+ </StrictMode>,
93
+ )
94
+
94
95
  ```
95
96
 
96
97
  ---
@@ -101,8 +102,10 @@ To support exit animations, modals must be rendered inside the same
101
102
  React tree as `AnimatePresence`.
102
103
 
103
104
  ```tsx
104
- import { ModalProvider, useModal, renderModals } from "react-flow-modal";
105
- import { motion, AnimatePresence } from "motion/react";
105
+ import { StrictMode } from 'react';
106
+ import { createRoot } from 'react-dom/client';
107
+ import { ModalHost, ModalProvider } from 'react-flow-modal';
108
+ import { AnimatePresence, motion } from 'motion/react';
106
109
 
107
110
  function ConfirmModal({
108
111
  onConfirm,
@@ -113,7 +116,7 @@ function ConfirmModal({
113
116
  }) {
114
117
  return (
115
118
  <motion.div
116
- key="modal"
119
+ key="confirm-modal-container"
117
120
  initial={{ opacity: 0 }}
118
121
  animate={{ opacity: 1 }}
119
122
  exit={{ opacity: 0 }}
@@ -127,7 +130,6 @@ function ConfirmModal({
127
130
  }}
128
131
  >
129
132
  <motion.div
130
- key="modal-content"
131
133
  initial={{ scale: 0.9, opacity: 0 }}
132
134
  animate={{ scale: 1, opacity: 1 }}
133
135
  exit={{ scale: 0.95, opacity: 0 }}
@@ -137,6 +139,7 @@ function ConfirmModal({
137
139
  padding: 24,
138
140
  borderRadius: 8,
139
141
  minWidth: 300,
142
+ color: "black",
140
143
  }}
141
144
  >
142
145
  <h3>Are you sure?</h3>
@@ -151,39 +154,24 @@ function ConfirmModal({
151
154
  );
152
155
  }
153
156
 
154
- function App() {
155
- const modal = useModal();
156
-
157
- const onClick = async () => {
158
- const result = await modal.open("confirm", (resolve) => (
159
- <ConfirmModal
160
- onConfirm={() => resolve(true)}
161
- onCancel={() => resolve(false)}
162
- />
163
- ));
164
-
165
- console.log("Result:", result);
166
- };
167
-
168
- return <button onClick={onClick}>Open Confirm Modal</button>;
169
- }
170
-
171
- function ModalRenderer() {
172
- return (
173
- <AnimatePresence>
174
- {renderModals()}
175
- </AnimatePresence>
176
- );
177
- }
157
+ ...
178
158
 
179
- export default function Root() {
180
- return (
159
+ createRoot(document.getElementById('root')!).render(
160
+ <StrictMode>
181
161
  <ModalProvider>
182
162
  <App />
183
- <ModalRenderer />
163
+ <ModalHost>
164
+ {(modals) => (
165
+ <AnimatePresence>
166
+ {modals}
167
+ </AnimatePresence>
168
+ )}
169
+ </ModalHost>
184
170
  </ModalProvider>
185
- );
186
- }
171
+ </StrictMode>,
172
+ )
173
+
174
+
187
175
  ```
188
176
 
189
177
  ---
@@ -210,11 +198,13 @@ Returns an object that controls the modal flow.
210
198
  }
211
199
  ```
212
200
 
213
- ### renderModals
201
+ ### ModalHost
214
202
  ```ts
215
- renderModals(): React.ReactNode
203
+ const ModalHost: FC<{
204
+ children?: (modals: ReactElement[]) => React.ReactNode;
205
+ }>;
216
206
  ```
217
- Renders the entire modal stack. This function should be rendered **once** in your React tree.
207
+ Renders the entire modal stack. This component should be rendered **once** in your React tree.
218
208
 
219
209
  ---
220
210
 
@@ -223,9 +213,6 @@ Renders the entire modal stack. This function should be rendered **once** in you
223
213
  > ⚠️ Always resolve or reject the promise.
224
214
  > Leaving it pending will block the async flow.
225
215
 
226
- > ⚠️ `renderModals()` should be rendered once.
227
- > Rendering it multiple times may result in duplicated modals.
228
-
229
216
  ---
230
217
 
231
218
  ## Why react-flow-modal?
package/dist/index.cjs CHANGED
@@ -22,79 +22,73 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ModalHost: () => ModalHost,
24
24
  ModalProvider: () => ModalProvider,
25
- useModal: () => useModal,
26
- useModalHost: () => useModalHost
25
+ useModal: () => useModal
27
26
  });
28
27
  module.exports = __toCommonJS(index_exports);
29
28
 
30
29
  // src/ModalProvider.tsx
31
- var import_react2 = require("react");
32
-
33
- // src/modalContext.ts
34
30
  var import_react = require("react");
35
- var ModalContext = (0, import_react.createContext)({
36
- stack: [],
37
- setStack: () => []
38
- });
39
-
40
- // src/ModalProvider.tsx
31
+ var import_zustand = require("zustand");
41
32
  var import_jsx_runtime = require("react/jsx-runtime");
42
- var ModalProvider = ({ children }) => {
43
- const [stack, setStack] = (0, import_react2.useState)([]);
44
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalContext.Provider, { value: { stack, setStack }, children });
45
- };
46
-
47
- // src/useModalContext.ts
48
- var import_react3 = require("react");
49
- var useModalContext = () => {
50
- const context = (0, import_react3.useContext)(ModalContext);
51
- if (!context) {
52
- throw new Error("useModalContext must be used within a ModalProvider");
53
- }
54
- return context;
55
- };
56
- var useModalHost = () => {
57
- const { stack } = useModalContext();
33
+ var createModalStore = (initialState) => (0, import_zustand.createStore)((set) => {
34
+ var _a;
58
35
  return {
59
- render: () => stack.map((item) => item)
36
+ stack: (_a = initialState == null ? void 0 : initialState.stack) != null ? _a : /* @__PURE__ */ new Map(),
37
+ appendStack: (key, element) => set((state) => ({ stack: new Map(state.stack).set(key, element) })),
38
+ removeStack: (key) => set((state) => {
39
+ const newStack = new Map(state.stack);
40
+ newStack.delete(key);
41
+ return { stack: newStack };
42
+ })
60
43
  };
44
+ });
45
+ var ModalStoreContext = (0, import_react.createContext)(null);
46
+ var ModalProvider = ({ children, initialState }) => {
47
+ const [store] = (0, import_react.useState)(createModalStore(initialState));
48
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalStoreContext.Provider, { value: store, children });
49
+ };
50
+ var useModalStore = (selector) => {
51
+ const store = (0, import_react.useContext)(ModalStoreContext);
52
+ if (!store) throw new Error("DashboardStoreProvider missing");
53
+ return (0, import_zustand.useStore)(store, selector);
61
54
  };
62
55
 
63
56
  // src/useModal.tsx
57
+ var import_react2 = require("react");
58
+ var import_jsx_runtime2 = require("react/jsx-runtime");
64
59
  var useModal = () => {
65
- const { setStack } = useModalContext();
66
- const pop = () => {
67
- setStack((prev) => prev.slice(0, -1));
68
- };
69
- const push = (element) => {
70
- setStack((prev) => [...prev, element]);
71
- };
60
+ const appendStack = useModalStore(
61
+ (state) => state.appendStack
62
+ );
63
+ const removeStack = useModalStore(
64
+ (state) => state.removeStack
65
+ );
72
66
  const open = (key, render) => {
73
67
  return new Promise((resolve, reject) => {
74
68
  const element = render((value) => {
75
69
  resolve(value);
76
- pop();
70
+ removeStack(key);
77
71
  }, (reason) => {
78
72
  reject(reason);
79
- pop();
73
+ removeStack(key);
80
74
  });
81
- push(element);
75
+ appendStack(key, /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.Fragment, { children: element }, `modal-${key}`));
82
76
  });
83
77
  };
84
78
  return { open };
85
79
  };
86
80
 
87
81
  // src/ModalHost.tsx
88
- var import_react4 = require("react");
89
- var import_jsx_runtime2 = require("react/jsx-runtime");
90
- var ModalHost = () => {
91
- const { render } = useModalHost();
92
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Fragment, { children: render() });
82
+ var ModalHost = ({ children }) => {
83
+ const stack = useModalStore(
84
+ (state) => state.stack
85
+ );
86
+ if (!children) return Array.from(stack.values());
87
+ return children(Array.from(stack.values()));
93
88
  };
94
89
  // Annotate the CommonJS export names for ESM import in node:
95
90
  0 && (module.exports = {
96
91
  ModalHost,
97
92
  ModalProvider,
98
- useModal,
99
- useModalHost
93
+ useModal
100
94
  });
package/dist/index.d.cts CHANGED
@@ -1,17 +1,21 @@
1
- import * as react from 'react';
2
- import react__default, { FC, PropsWithChildren } from 'react';
1
+ import React$1, { PropsWithChildren, ReactElement, FC } from 'react';
3
2
 
4
- declare const ModalProvider: FC<PropsWithChildren>;
3
+ type ModalState = {
4
+ stack: Map<string, ReactElement>;
5
+ appendStack: (key: string, element: ReactElement) => void;
6
+ removeStack: (key: string) => void;
7
+ };
8
+ type ProviderProps = {
9
+ initialState?: Partial<ModalState>;
10
+ };
11
+ declare const ModalProvider: React$1.FC<PropsWithChildren<ProviderProps>>;
5
12
 
6
13
  declare const useModal: () => {
7
- open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
14
+ open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => React$1.ReactNode) => Promise<T>;
8
15
  };
9
16
 
10
- /** @deprecated use renderModals instead */
11
- declare const ModalHost: FC;
12
-
13
- declare const useModalHost: () => {
14
- render: () => react.ReactNode[];
15
- };
17
+ declare const ModalHost: FC<{
18
+ children?: (modals: ReactElement[]) => React.ReactNode;
19
+ }>;
16
20
 
17
- export { ModalHost, ModalProvider, useModal, useModalHost };
21
+ export { ModalHost, ModalProvider, useModal };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
- import * as react from 'react';
2
- import react__default, { FC, PropsWithChildren } from 'react';
1
+ import React$1, { PropsWithChildren, ReactElement, FC } from 'react';
3
2
 
4
- declare const ModalProvider: FC<PropsWithChildren>;
3
+ type ModalState = {
4
+ stack: Map<string, ReactElement>;
5
+ appendStack: (key: string, element: ReactElement) => void;
6
+ removeStack: (key: string) => void;
7
+ };
8
+ type ProviderProps = {
9
+ initialState?: Partial<ModalState>;
10
+ };
11
+ declare const ModalProvider: React$1.FC<PropsWithChildren<ProviderProps>>;
5
12
 
6
13
  declare const useModal: () => {
7
- open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => react__default.ReactNode) => Promise<T>;
14
+ open: <T>(key: string, render: (resolve: (value: T) => void, reject: (reason?: unknown) => void) => React$1.ReactNode) => Promise<T>;
8
15
  };
9
16
 
10
- /** @deprecated use renderModals instead */
11
- declare const ModalHost: FC;
12
-
13
- declare const useModalHost: () => {
14
- render: () => react.ReactNode[];
15
- };
17
+ declare const ModalHost: FC<{
18
+ children?: (modals: ReactElement[]) => React.ReactNode;
19
+ }>;
16
20
 
17
- export { ModalHost, ModalProvider, useModal, useModalHost };
21
+ export { ModalHost, ModalProvider, useModal };
package/dist/index.js CHANGED
@@ -1,70 +1,65 @@
1
1
  // src/ModalProvider.tsx
2
- import { useState } from "react";
3
-
4
- // src/modalContext.ts
5
- import { createContext } from "react";
6
- var ModalContext = createContext({
7
- stack: [],
8
- setStack: () => []
9
- });
10
-
11
- // src/ModalProvider.tsx
2
+ import { createContext, useContext, useState } from "react";
3
+ import { createStore, useStore } from "zustand";
12
4
  import { jsx } from "react/jsx-runtime";
13
- var ModalProvider = ({ children }) => {
14
- const [stack, setStack] = useState([]);
15
- return /* @__PURE__ */ jsx(ModalContext.Provider, { value: { stack, setStack }, children });
16
- };
17
-
18
- // src/useModalContext.ts
19
- import { useContext } from "react";
20
- var useModalContext = () => {
21
- const context = useContext(ModalContext);
22
- if (!context) {
23
- throw new Error("useModalContext must be used within a ModalProvider");
24
- }
25
- return context;
26
- };
27
- var useModalHost = () => {
28
- const { stack } = useModalContext();
5
+ var createModalStore = (initialState) => createStore((set) => {
6
+ var _a;
29
7
  return {
30
- render: () => stack.map((item) => item)
8
+ stack: (_a = initialState == null ? void 0 : initialState.stack) != null ? _a : /* @__PURE__ */ new Map(),
9
+ appendStack: (key, element) => set((state) => ({ stack: new Map(state.stack).set(key, element) })),
10
+ removeStack: (key) => set((state) => {
11
+ const newStack = new Map(state.stack);
12
+ newStack.delete(key);
13
+ return { stack: newStack };
14
+ })
31
15
  };
16
+ });
17
+ var ModalStoreContext = createContext(null);
18
+ var ModalProvider = ({ children, initialState }) => {
19
+ const [store] = useState(createModalStore(initialState));
20
+ return /* @__PURE__ */ jsx(ModalStoreContext.Provider, { value: store, children });
21
+ };
22
+ var useModalStore = (selector) => {
23
+ const store = useContext(ModalStoreContext);
24
+ if (!store) throw new Error("DashboardStoreProvider missing");
25
+ return useStore(store, selector);
32
26
  };
33
27
 
34
28
  // src/useModal.tsx
29
+ import { Fragment } from "react";
30
+ import { jsx as jsx2 } from "react/jsx-runtime";
35
31
  var useModal = () => {
36
- const { setStack } = useModalContext();
37
- const pop = () => {
38
- setStack((prev) => prev.slice(0, -1));
39
- };
40
- const push = (element) => {
41
- setStack((prev) => [...prev, element]);
42
- };
32
+ const appendStack = useModalStore(
33
+ (state) => state.appendStack
34
+ );
35
+ const removeStack = useModalStore(
36
+ (state) => state.removeStack
37
+ );
43
38
  const open = (key, render) => {
44
39
  return new Promise((resolve, reject) => {
45
40
  const element = render((value) => {
46
41
  resolve(value);
47
- pop();
42
+ removeStack(key);
48
43
  }, (reason) => {
49
44
  reject(reason);
50
- pop();
45
+ removeStack(key);
51
46
  });
52
- push(element);
47
+ appendStack(key, /* @__PURE__ */ jsx2(Fragment, { children: element }, `modal-${key}`));
53
48
  });
54
49
  };
55
50
  return { open };
56
51
  };
57
52
 
58
53
  // src/ModalHost.tsx
59
- import { Fragment } from "react";
60
- import { jsx as jsx2 } from "react/jsx-runtime";
61
- var ModalHost = () => {
62
- const { render } = useModalHost();
63
- return /* @__PURE__ */ jsx2(Fragment, { children: render() });
54
+ var ModalHost = ({ children }) => {
55
+ const stack = useModalStore(
56
+ (state) => state.stack
57
+ );
58
+ if (!children) return Array.from(stack.values());
59
+ return children(Array.from(stack.values()));
64
60
  };
65
61
  export {
66
62
  ModalHost,
67
63
  ModalProvider,
68
- useModal,
69
- useModalHost
64
+ useModal
70
65
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-flow-modal",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Promise-based modal flows for React",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,5 +39,8 @@
39
39
  "tsup": "^8.5.1",
40
40
  "typescript": "^5.9.3"
41
41
  },
42
- "packageManager": "pnpm@10.27.0"
42
+ "packageManager": "pnpm@10.27.0",
43
+ "dependencies": {
44
+ "zustand": "^5.0.11"
45
+ }
43
46
  }
package/src/ModalHost.tsx CHANGED
@@ -1,13 +1,13 @@
1
- import { FC, Fragment } from 'react';
2
- import { useModalHost } from './useModalContext';
1
+ import { FC, isValidElement, ReactElement } from 'react';
2
+ import { useModalStore } from './ModalProvider';
3
3
 
4
- /** @deprecated use renderModals instead */
5
- export const ModalHost: FC = () => {
6
- const { render } = useModalHost();
7
4
 
8
- return (
9
- <Fragment>
10
- {render()}
11
- </Fragment>
5
+ export const ModalHost: FC<{ children?: (modals: ReactElement[]) => React.ReactNode }> = ({ children }) => {
6
+ const stack = useModalStore(
7
+ (state) => state.stack
12
8
  );
9
+
10
+
11
+ if (!children) return Array.from(stack.values());
12
+ return children(Array.from(stack.values()));
13
13
  };
@@ -1,12 +1,52 @@
1
- import React, { FC, PropsWithChildren, useState } from "react";
2
- import { ModalContext } from "./modalContext";
1
+ 'use client';
3
2
 
4
- export const ModalProvider: FC<PropsWithChildren> = ({ children }) => {
5
- const [stack, setStack] = useState<React.ReactNode[]>([]);
3
+
4
+ import React, { createContext, PropsWithChildren, ReactElement, useContext, useState } from 'react';
5
+ import { createStore, ExtractState, StoreApi, useStore } from 'zustand';
6
+
7
+ type ModalState = {
8
+ stack: Map<string, ReactElement>;
9
+ appendStack: (key: string, element: ReactElement) => void;
10
+ removeStack: (key: string) => void;
11
+ };
12
+
13
+
14
+
15
+ const createModalStore = (initialState?: Partial<ModalState>) => createStore<ModalState>((set) => {
16
+ return {
17
+ stack: initialState?.stack ?? new Map<string, ReactElement>(),
18
+
19
+ appendStack: (key: string, element: ReactElement) => set((state) => ({ stack: new Map(state.stack).set(key, element) })),
20
+
21
+ removeStack: (key: string) => set((state) => {
22
+ const newStack = new Map(state.stack);
23
+ newStack.delete(key);
24
+ return { stack: newStack };
25
+ }),
26
+ }
27
+ });
28
+
29
+ const ModalStoreContext = createContext<StoreApi<ModalState> | null>(null);
30
+
31
+ type ProviderProps = {
32
+ initialState?: Partial<ModalState>;
33
+ }
34
+
35
+
36
+ export const ModalProvider: React.FC<PropsWithChildren<ProviderProps>> = ({ children, initialState }) => {
37
+ // 여러번 호출되어도 store가 하나만 생성되도록 함
38
+ const [store] = useState(createModalStore(initialState));
6
39
 
7
40
  return (
8
- <ModalContext.Provider value={{ stack, setStack }}>
41
+ <ModalStoreContext.Provider value={store}>
9
42
  {children}
10
- </ModalContext.Provider>
43
+ </ModalStoreContext.Provider>
11
44
  );
45
+ };
46
+
47
+
48
+ export const useModalStore = <T,>(selector: (state: ExtractState<StoreApi<ModalState>>) => T) => {
49
+ const store = useContext(ModalStoreContext);
50
+ if (!store) throw new Error('DashboardStoreProvider missing');
51
+ return useStore(store, selector);
12
52
  };
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { ModalProvider } from './ModalProvider';
2
2
  export { useModal } from './useModal';
3
- export { ModalHost } from './ModalHost';
4
- export { useModalHost } from './useModalContext';
3
+ export { ModalHost } from './ModalHost';
package/src/useModal.tsx CHANGED
@@ -1,16 +1,15 @@
1
1
  import React, { Fragment } from "react";
2
- import { useModalContext } from "./useModalContext";
2
+ import { useModalStore } from "./ModalProvider";
3
3
 
4
4
  export const useModal = () => {
5
- const { setStack } = useModalContext();
5
+ const appendStack = useModalStore(
6
+ (state) => state.appendStack
7
+ );
6
8
 
7
- const pop = () => {
8
- setStack((prev) => prev.slice(0, -1));
9
- };
9
+ const removeStack = useModalStore(
10
+ (state) => state.removeStack
11
+ );
10
12
 
11
- const push = (element: React.ReactNode) => {
12
- setStack((prev) => [...prev, element]);
13
- };
14
13
 
15
14
  /**
16
15
  * 모달을 열고 닫는 함수
@@ -24,12 +23,12 @@ export const useModal = () => {
24
23
  return new Promise<T>((resolve, reject) => {
25
24
  const element = render((value) => {
26
25
  resolve(value);
27
- pop();
26
+ removeStack(key);
28
27
  }, (reason) => {
29
28
  reject(reason);
30
- pop();
29
+ removeStack(key);
31
30
  });
32
- push(element);
31
+ appendStack(key, <Fragment key={`modal-${key}`}>{element}</Fragment>);
33
32
  });
34
33
  };
35
34
 
@@ -1,9 +0,0 @@
1
- import { createContext, Dispatch, SetStateAction } from "react";
2
-
3
- export const ModalContext = createContext<{
4
- stack: React.ReactNode[];
5
- setStack: Dispatch<SetStateAction<React.ReactNode[]>>
6
- }>({
7
- stack: [],
8
- setStack: () => [] as React.ReactNode[],
9
- });
@@ -1,20 +0,0 @@
1
- import { useContext } from "react";
2
- import { ModalContext } from "./modalContext";
3
-
4
- export const useModalContext = () => {
5
- const context = useContext(ModalContext);
6
-
7
- if (!context) {
8
- throw new Error('useModalContext must be used within a ModalProvider');
9
- }
10
-
11
- return context;
12
- };
13
-
14
- export const useModalHost = () => {
15
- const { stack } = useModalContext();
16
-
17
- return {
18
- render: () => stack.map((item) => (item)),
19
- };
20
- };