@violice/rmu 0.2.7 → 0.2.8

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.
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { RMUProvider } from './rmu-provider';
5
+ import { RMUOutlet } from './rmu-outlet';
6
+ import { openModal, closeModal } from './events';
7
+ import { emitter } from './emitter';
8
+
9
+ describe('Integration: Full RMU Workflow', () => {
10
+ beforeEach(() => {
11
+ // Clear all emitter subscriptions before each test
12
+ emitter._clear();
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Clean up the DOM after each test
17
+ cleanup();
18
+ });
19
+
20
+ it('should handle complete workflow with single outlet', async () => {
21
+ render(
22
+ <RMUProvider>
23
+ <div data-testid="app">App Content</div>
24
+ <RMUOutlet />
25
+ </RMUProvider>
26
+ );
27
+
28
+ // Wait for outlet to be registered
29
+ await waitFor(() => {
30
+ expect(document.body).toBeDefined();
31
+ });
32
+
33
+ // Initial state - app content visible, no modals
34
+ expect(screen.getByTestId('app')).toBeDefined();
35
+ expect(screen.queryByTestId('modal-1')).toBeNull();
36
+
37
+ // Open first modal
38
+ let modal1Info: { modalId: string; outletId: string };
39
+ act(() => {
40
+ modal1Info = openModal(<div data-testid="modal-1">First Modal</div>);
41
+ });
42
+
43
+ await waitFor(() => {
44
+ expect(screen.getByTestId('modal-1')).toBeDefined();
45
+ });
46
+
47
+ // Open second modal
48
+ let modal2Info: { modalId: string; outletId: string };
49
+ act(() => {
50
+ modal2Info = openModal(<div data-testid="modal-2">Second Modal</div>);
51
+ });
52
+
53
+ await waitFor(() => {
54
+ expect(screen.getByTestId('modal-1')).toBeDefined();
55
+ expect(screen.getByTestId('modal-2')).toBeDefined();
56
+ });
57
+
58
+ // Close first modal
59
+ act(() => {
60
+ closeModal(modal1Info);
61
+ });
62
+
63
+ await waitFor(() => {
64
+ expect(screen.queryByTestId('modal-1')).toBeNull();
65
+ expect(screen.getByTestId('modal-2')).toBeDefined();
66
+ });
67
+
68
+ // Close second modal
69
+ act(() => {
70
+ closeModal(modal2Info);
71
+ });
72
+
73
+ await waitFor(() => {
74
+ expect(screen.queryByTestId('modal-1')).toBeNull();
75
+ expect(screen.queryByTestId('modal-2')).toBeNull();
76
+ });
77
+
78
+ // App content still visible
79
+ expect(screen.getByTestId('app')).toBeDefined();
80
+ });
81
+
82
+ it('should handle workflow with multiple outlets', async () => {
83
+ render(
84
+ <RMUProvider>
85
+ <div data-testid="sidebar">
86
+ <RMUOutlet outletId="sidebar-outlet" />
87
+ </div>
88
+ <div data-testid="main">
89
+ <RMUOutlet outletId="main-outlet" />
90
+ </div>
91
+ </RMUProvider>
92
+ );
93
+
94
+ // Wait for outlets to be registered
95
+ await waitFor(() => {
96
+ expect(document.body).toBeDefined();
97
+ });
98
+
99
+ // Open modals in different outlets
100
+ let sidebarModalInfo: { modalId: string; outletId: string };
101
+ let mainModalInfo: { modalId: string; outletId: string };
102
+
103
+ act(() => {
104
+ sidebarModalInfo = openModal(
105
+ <div data-testid="sidebar-modal">Sidebar Modal</div>,
106
+ { outletId: 'sidebar-outlet' }
107
+ );
108
+ mainModalInfo = openModal(
109
+ <div data-testid="main-modal">Main Modal</div>,
110
+ { outletId: 'main-outlet' }
111
+ );
112
+ });
113
+
114
+ await waitFor(() => {
115
+ expect(screen.getByTestId('sidebar-modal')).toBeDefined();
116
+ expect(screen.getByTestId('main-modal')).toBeDefined();
117
+ });
118
+
119
+ // Verify modals are in correct outlets
120
+ expect(screen.getByTestId('sidebar').contains(screen.getByTestId('sidebar-modal'))).toBe(true);
121
+ expect(screen.getByTestId('main').contains(screen.getByTestId('main-modal'))).toBe(true);
122
+
123
+ // Close sidebar modal only
124
+ act(() => {
125
+ closeModal(sidebarModalInfo);
126
+ });
127
+
128
+ await waitFor(() => {
129
+ expect(screen.queryByTestId('sidebar-modal')).toBeNull();
130
+ expect(screen.getByTestId('main-modal')).toBeDefined();
131
+ });
132
+
133
+ // Close main modal
134
+ act(() => {
135
+ closeModal(mainModalInfo);
136
+ });
137
+
138
+ await waitFor(() => {
139
+ expect(screen.queryByTestId('sidebar-modal')).toBeNull();
140
+ expect(screen.queryByTestId('main-modal')).toBeNull();
141
+ });
142
+ });
143
+
144
+ it('should handle rapid open/close operations', async () => {
145
+ render(
146
+ <RMUProvider>
147
+ <RMUOutlet />
148
+ </RMUProvider>
149
+ );
150
+
151
+ // Wait for outlet to be registered
152
+ await waitFor(() => {
153
+ expect(document.body).toBeDefined();
154
+ });
155
+
156
+ const modals: Array<{ modalId: string; outletId: string }> = [];
157
+
158
+ // Open 5 modals sequentially (with small delays to avoid ID collision)
159
+ for (let i = 0; i < 5; i++) {
160
+ act(() => {
161
+ modals.push(
162
+ openModal(<div data-testid={`rapid-modal-${i}`}>Modal {i}</div>)
163
+ );
164
+ });
165
+ // Small delay to ensure unique IDs
166
+ await new Promise(resolve => setTimeout(resolve, 10));
167
+ }
168
+
169
+ await waitFor(() => {
170
+ for (let i = 0; i < 5; i++) {
171
+ expect(screen.getByTestId(`rapid-modal-${i}`)).toBeDefined();
172
+ }
173
+ });
174
+
175
+ // Close all modals
176
+ act(() => {
177
+ modals.forEach(modalInfo => closeModal(modalInfo));
178
+ });
179
+
180
+ await waitFor(() => {
181
+ for (let i = 0; i < 5; i++) {
182
+ expect(screen.queryByTestId(`rapid-modal-${i}`)).toBeNull();
183
+ }
184
+ });
185
+ });
186
+
187
+ it('should handle modal with complex React components', async () => {
188
+ const ComplexComponent = ({ title, onAction }: { title: string; onAction?: () => void }) => (
189
+ <div data-testid="complex-modal">
190
+ <h1>{title}</h1>
191
+ <button data-testid="action-btn" onClick={onAction}>
192
+ Action
193
+ </button>
194
+ <ul>
195
+ <li>Item 1</li>
196
+ <li>Item 2</li>
197
+ </ul>
198
+ </div>
199
+ );
200
+
201
+ render(
202
+ <RMUProvider>
203
+ <RMUOutlet />
204
+ </RMUProvider>
205
+ );
206
+
207
+ // Wait for outlet to be registered
208
+ await waitFor(() => {
209
+ expect(document.body).toBeDefined();
210
+ });
211
+
212
+ const actionHandler = vi.fn();
213
+
214
+ act(() => {
215
+ openModal(<ComplexComponent title="Test Modal" onAction={actionHandler} />);
216
+ });
217
+
218
+ await waitFor(() => {
219
+ expect(screen.getByTestId('complex-modal')).toBeDefined();
220
+ expect(screen.getByText('Test Modal')).toBeDefined();
221
+ expect(screen.getByTestId('action-btn')).toBeDefined();
222
+ expect(screen.getByText('Item 1')).toBeDefined();
223
+ expect(screen.getByText('Item 2')).toBeDefined();
224
+ });
225
+ });
226
+
227
+ it('should handle outlet removal and re-addition', async () => {
228
+ const { unmount } = render(
229
+ <RMUProvider>
230
+ <RMUOutlet outletId="dynamic-outlet" />
231
+ </RMUProvider>
232
+ );
233
+
234
+ // Wait for outlet to be registered
235
+ await waitFor(() => {
236
+ expect(document.body).toBeDefined();
237
+ });
238
+
239
+ // Open a modal
240
+ let modalInfo: { modalId: string; outletId: string };
241
+ act(() => {
242
+ modalInfo = openModal(
243
+ <div data-testid="dynamic-modal">Dynamic Modal</div>,
244
+ { outletId: 'dynamic-outlet' }
245
+ );
246
+ });
247
+
248
+ await waitFor(() => {
249
+ expect(screen.getByTestId('dynamic-modal')).toBeDefined();
250
+ });
251
+
252
+ // Unmount the outlet
253
+ unmount();
254
+
255
+ // Re-render with a new outlet (need to use render again after unmount)
256
+ render(
257
+ <RMUProvider>
258
+ <RMUOutlet outletId="dynamic-outlet" />
259
+ </RMUProvider>
260
+ );
261
+
262
+ // Modal should not be visible after outlet removal and re-addition
263
+ // because the outlet was removed from state
264
+ expect(screen.queryByTestId('dynamic-modal')).toBeNull();
265
+ });
266
+ });
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, waitFor, act, cleanup } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { RMUProvider } from './rmu-provider';
5
+ import { RMUOutlet } from './rmu-outlet';
6
+ import { openModal, closeModal } from './events';
7
+ import { emitter } from './emitter';
8
+
9
+ describe('RMUOutlet', () => {
10
+ beforeEach(() => {
11
+ // Clear all emitter subscriptions before each test
12
+ emitter._clear();
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Clean up the DOM after each test
17
+ cleanup();
18
+ });
19
+
20
+ it('should throw when used outside RMUProvider', () => {
21
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
22
+
23
+ expect(() => {
24
+ render(<RMUOutlet />);
25
+ }).toThrow('RMUProvider not found in component tree');
26
+
27
+ consoleSpy.mockRestore();
28
+ });
29
+
30
+ it('should render with default outlet', () => {
31
+ render(
32
+ <RMUProvider>
33
+ <div data-testid="content">content</div>
34
+ <RMUOutlet />
35
+ </RMUProvider>
36
+ );
37
+
38
+ expect(screen.getByTestId('content')).toBeDefined();
39
+ });
40
+
41
+ it('should render with custom outlet id', () => {
42
+ render(
43
+ <RMUProvider>
44
+ <RMUOutlet outletId="custom-outlet" />
45
+ </RMUProvider>
46
+ );
47
+
48
+ // The outlet should be rendered without errors
49
+ expect(document.body).toBeDefined();
50
+ });
51
+
52
+ it('should render multiple outlets', () => {
53
+ render(
54
+ <RMUProvider>
55
+ <RMUOutlet outletId="outlet-1" />
56
+ <RMUOutlet outletId="outlet-2" />
57
+ </RMUProvider>
58
+ );
59
+
60
+ // Both outlets should be rendered without errors
61
+ expect(document.body).toBeDefined();
62
+ });
63
+
64
+ it('should render modal when opened via events', async () => {
65
+ render(
66
+ <RMUProvider>
67
+ <RMUOutlet />
68
+ </RMUProvider>
69
+ );
70
+
71
+ // Wait for outlet to be registered via useEffect
72
+ await waitFor(() => {
73
+ expect(document.body).toBeDefined();
74
+ });
75
+
76
+ const modalContent = <div data-testid="modal-content">Modal Content</div>;
77
+
78
+ act(() => {
79
+ openModal(modalContent);
80
+ });
81
+
82
+ await waitFor(() => {
83
+ expect(screen.getByTestId('modal-content')).toBeDefined();
84
+ });
85
+ });
86
+
87
+ it('should render modal in custom outlet', async () => {
88
+ render(
89
+ <RMUProvider>
90
+ <RMUOutlet outletId="custom-outlet" />
91
+ </RMUProvider>
92
+ );
93
+
94
+ // Wait for outlet to be registered via useEffect
95
+ await waitFor(() => {
96
+ expect(document.body).toBeDefined();
97
+ });
98
+
99
+ const modalContent = <div data-testid="custom-modal">Custom Modal</div>;
100
+
101
+ act(() => {
102
+ openModal(modalContent, { outletId: 'custom-outlet' });
103
+ });
104
+
105
+ await waitFor(() => {
106
+ expect(screen.getByTestId('custom-modal')).toBeDefined();
107
+ });
108
+ });
109
+
110
+ it('should not render modal in wrong outlet', async () => {
111
+ render(
112
+ <RMUProvider>
113
+ <RMUOutlet outletId="outlet-1" />
114
+ </RMUProvider>
115
+ );
116
+
117
+ // Wait for outlet to be registered via useEffect
118
+ await waitFor(() => {
119
+ expect(document.body).toBeDefined();
120
+ });
121
+
122
+ const modalContent = <div data-testid="wrong-modal">Wrong Modal</div>;
123
+
124
+ // Opening modal in non-existent outlet should throw
125
+ expect(() => {
126
+ act(() => {
127
+ openModal(modalContent, { outletId: 'outlet-2' });
128
+ });
129
+ }).toThrow('Outlet with id outlet-2 not found');
130
+
131
+ // Modal should not be rendered in outlet-1
132
+ expect(screen.queryByTestId('wrong-modal')).toBeNull();
133
+ });
134
+
135
+ it('should remove modal when closed', async () => {
136
+ render(
137
+ <RMUProvider>
138
+ <RMUOutlet />
139
+ </RMUProvider>
140
+ );
141
+
142
+ // Wait for outlet to be registered via useEffect
143
+ await waitFor(() => {
144
+ expect(document.body).toBeDefined();
145
+ });
146
+
147
+ const modalContent = <div data-testid="closable-modal">Closable Modal</div>;
148
+
149
+ let modalInfo: { modalId: string; outletId: string };
150
+
151
+ act(() => {
152
+ modalInfo = openModal(modalContent);
153
+ });
154
+
155
+ await waitFor(() => {
156
+ expect(screen.getByTestId('closable-modal')).toBeDefined();
157
+ });
158
+
159
+ act(() => {
160
+ closeModal(modalInfo);
161
+ });
162
+
163
+ await waitFor(() => {
164
+ expect(screen.queryByTestId('closable-modal')).toBeNull();
165
+ });
166
+ });
167
+
168
+ it('should render multiple modals in same outlet', async () => {
169
+ render(
170
+ <RMUProvider>
171
+ <RMUOutlet />
172
+ </RMUProvider>
173
+ );
174
+
175
+ // Wait for outlet to be registered via useEffect
176
+ await waitFor(() => {
177
+ expect(document.body).toBeDefined();
178
+ });
179
+
180
+ // Open first modal
181
+ act(() => {
182
+ openModal(<div data-testid="modal-1">Modal 1</div>);
183
+ });
184
+
185
+ await waitFor(() => {
186
+ expect(screen.getByTestId('modal-1')).toBeDefined();
187
+ });
188
+
189
+ // Open second modal after first one is rendered
190
+ act(() => {
191
+ openModal(<div data-testid="modal-2">Modal 2</div>);
192
+ });
193
+
194
+ await waitFor(() => {
195
+ expect(screen.getByTestId('modal-1')).toBeDefined();
196
+ expect(screen.getByTestId('modal-2')).toBeDefined();
197
+ });
198
+ });
199
+
200
+ it('should cleanup outlet on unmount', () => {
201
+ const { unmount } = render(
202
+ <RMUProvider>
203
+ <RMUOutlet outletId="cleanup-outlet" />
204
+ </RMUProvider>
205
+ );
206
+
207
+ // Unmount the outlet
208
+ unmount();
209
+
210
+ // The component should unmount without errors
211
+ expect(document.body).toBeDefined();
212
+ });
213
+ });
@@ -1,7 +1,8 @@
1
1
  import React, { Fragment, useContext, useEffect } from 'react';
2
2
  import { RMUContext } from './rmu-context';
3
+ import { RMU_DEFAULT_OUTLET_ID } from './constants';
3
4
 
4
- export const RMUOutlet = ({ outletId = 'rmu-default-outlet' }) => {
5
+ export const RMUOutlet = ({ outletId = RMU_DEFAULT_OUTLET_ID }) => {
5
6
  const ctx = useContext(RMUContext);
6
7
 
7
8
  if (!ctx) {
@@ -15,7 +16,7 @@ export const RMUOutlet = ({ outletId = 'rmu-default-outlet' }) => {
15
16
  return () => {
16
17
  removeOutlet(outletId);
17
18
  };
18
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
19
+ }, []);
19
20
 
20
21
  const modals = outlets[outletId] ?? {};
21
22
 
@@ -0,0 +1,15 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { RMUProvider } from './rmu-provider';
4
+
5
+ describe('RMUProvider', () => {
6
+ it('should render children', () => {
7
+ render(
8
+ <RMUProvider>
9
+ <div data-testid="child">Child Content</div>
10
+ </RMUProvider>
11
+ );
12
+
13
+ expect(screen.getByTestId('child')).toBeDefined();
14
+ });
15
+ });
package/src/types.ts CHANGED
@@ -1,23 +1,27 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
+ export enum RMUEventType {
4
+ OpenModal = 'RMU:OPEN_MODAL',
5
+ CloseModal = 'RMU:CLOSE_MODAL',
6
+ }
7
+
3
8
  export type RMUContextState = {
4
9
  outlets: Record<string, Record<string, ReactNode>>;
5
- openModal: ({
6
- modalId,
7
- modalComponent,
8
- outletId,
9
- }: {
10
- modalId: string;
11
- modalComponent: ReactNode;
12
- outletId: string;
13
- }) => void;
14
- closeModal: ({
15
- modalId,
16
- outletId,
17
- }: {
18
- modalId: string;
19
- outletId: string;
20
- }) => void;
10
+ openModal: (payload: OpenModalPayload) => void;
11
+ closeModal: (payload: CloseModalPayload) => void;
21
12
  addOutlet: (outletId: string) => void;
22
13
  removeOutlet: (outletId: string) => void;
23
14
  };
15
+
16
+ export type OpenModalPayload = {
17
+ modalId: string;
18
+ modalComponent: ReactNode;
19
+ outletId: string;
20
+ };
21
+
22
+ export type CloseModalPayload = {
23
+ modalId: string;
24
+ outletId: string;
25
+ };
26
+
27
+ export type RMUEventPayload = OpenModalPayload | CloseModalPayload;
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useRMUEvents } from './use-rmu-events';
4
+ import { emitter } from './emitter';
5
+ import { RMU_EVENTS } from './constants';
6
+ import { RMUContextState } from './types';
7
+ import React from 'react';
8
+
9
+ describe('useRMUEvents', () => {
10
+ const mockOpenModal = vi.fn();
11
+ const mockCloseModal = vi.fn();
12
+ const mockAddOutlet = vi.fn();
13
+ const mockRemoveOutlet = vi.fn();
14
+
15
+ const mockCtx: RMUContextState = {
16
+ outlets: {},
17
+ openModal: mockOpenModal,
18
+ closeModal: mockCloseModal,
19
+ addOutlet: mockAddOutlet,
20
+ removeOutlet: mockRemoveOutlet,
21
+ };
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ // Clear all emitter subscriptions before each test
26
+ emitter._clear();
27
+ });
28
+
29
+ it('should subscribe to open and close events on mount', () => {
30
+ renderHook(() => useRMUEvents(mockCtx));
31
+
32
+ // Emit open event and verify it calls openModal
33
+ const mockComponent = React.createElement('div', null, 'Test');
34
+ emitter.emit(RMU_EVENTS.open, {
35
+ modalId: 'test-modal',
36
+ modalComponent: mockComponent,
37
+ outletId: 'test-outlet',
38
+ });
39
+
40
+ expect(mockOpenModal).toHaveBeenCalledWith({
41
+ modalId: 'test-modal',
42
+ modalComponent: mockComponent,
43
+ outletId: 'test-outlet',
44
+ });
45
+
46
+ // Emit close event and verify it calls closeModal
47
+ emitter.emit(RMU_EVENTS.close, {
48
+ modalId: 'test-modal',
49
+ outletId: 'test-outlet',
50
+ });
51
+
52
+ expect(mockCloseModal).toHaveBeenCalledWith({
53
+ modalId: 'test-modal',
54
+ outletId: 'test-outlet',
55
+ });
56
+ });
57
+
58
+ it('should unsubscribe from events on unmount', () => {
59
+ const { unmount } = renderHook(() => useRMUEvents(mockCtx));
60
+
61
+ // Clear mocks after subscription
62
+ vi.clearAllMocks();
63
+
64
+ // Unmount the hook
65
+ unmount();
66
+
67
+ // Emit events after unmount - handlers should not be called
68
+ const mockComponent = React.createElement('div', null, 'Test');
69
+ emitter.emit(RMU_EVENTS.open, {
70
+ modalId: 'test-modal',
71
+ modalComponent: mockComponent,
72
+ outletId: 'test-outlet',
73
+ });
74
+
75
+ emitter.emit(RMU_EVENTS.close, {
76
+ modalId: 'test-modal',
77
+ outletId: 'test-outlet',
78
+ });
79
+
80
+ expect(mockOpenModal).not.toHaveBeenCalled();
81
+ expect(mockCloseModal).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('should handle multiple open and close events', () => {
85
+ renderHook(() => useRMUEvents(mockCtx));
86
+
87
+ const mockComponent = React.createElement('div', null, 'Test');
88
+
89
+ // Emit multiple open events
90
+ emitter.emit(RMU_EVENTS.open, {
91
+ modalId: 'modal-1',
92
+ modalComponent: mockComponent,
93
+ outletId: 'outlet-1',
94
+ });
95
+
96
+ emitter.emit(RMU_EVENTS.open, {
97
+ modalId: 'modal-2',
98
+ modalComponent: mockComponent,
99
+ outletId: 'outlet-1',
100
+ });
101
+
102
+ expect(mockOpenModal).toHaveBeenCalledTimes(2);
103
+
104
+ // Emit multiple close events
105
+ emitter.emit(RMU_EVENTS.close, {
106
+ modalId: 'modal-1',
107
+ outletId: 'outlet-1',
108
+ });
109
+
110
+ emitter.emit(RMU_EVENTS.close, {
111
+ modalId: 'modal-2',
112
+ outletId: 'outlet-1',
113
+ });
114
+
115
+ expect(mockCloseModal).toHaveBeenCalledTimes(2);
116
+ });
117
+ });
@@ -1,12 +1,12 @@
1
1
  import { useEffect } from 'react';
2
- import { RMUContextState } from './types';
3
- import { RMU_EVENTS } from './events';
2
+ import { RMUContextState, OpenModalPayload, CloseModalPayload } from './types';
3
+ import { RMU_EVENTS } from './constants';
4
4
  import { emitter } from './emitter';
5
5
 
6
6
  export const useRMUEvents = (ctx: RMUContextState) => {
7
7
  const events = {
8
- open: (payload: any) => ctx.openModal(payload),
9
- close: (payload: any) => ctx.closeModal(payload),
8
+ open: (payload: OpenModalPayload) => ctx.openModal(payload),
9
+ close: (payload: CloseModalPayload) => ctx.closeModal(payload),
10
10
  };
11
11
 
12
12
  useEffect(() => {
@@ -19,5 +19,5 @@ export const useRMUEvents = (ctx: RMUContextState) => {
19
19
  emitter.unsubscribe(RMU_EVENTS[event], events[event]);
20
20
  });
21
21
  };
22
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
22
+ }, []);
23
23
  };