@swmansion/react-native-bottom-sheet 0.9.0 → 0.9.1-next.1

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
@@ -10,6 +10,7 @@ React Native.
10
10
  ## Highlights
11
11
 
12
12
  - Native implementation for optimal performance.
13
+ - Both inline and modal sheet components.
13
14
  - Bring your own sheet surface.
14
15
  - Dynamic, content‍-‍based sizing out of the box.
15
16
  - Automatic handling of vertically scrollable children.
@@ -70,7 +71,8 @@ const insets = useSafeAreaInsets();
70
71
 
71
72
  ### Modal
72
73
 
73
- `ModalBottomSheet` renders above other content with a scrim.
74
+ `ModalBottomSheet` renders above other content with an optional scrim
75
+ (transparent by default).
74
76
 
75
77
  ```tsx
76
78
  const [index, setIndex] = useState(0);
@@ -106,6 +108,25 @@ its color:
106
108
  </ModalBottomSheet>
107
109
  ```
108
110
 
111
+ ### Scrollable negotiation
112
+
113
+ By default, the sheet coordinates vertical gestures with nested scrollables,
114
+ such as `ScrollView` and&nbsp;`FlatList`.
115
+
116
+ If you want gestures that start inside a nested scrollable to stay with that
117
+ scrollable even when it cannot scroll any further,
118
+ set&nbsp;`disableScrollableNegotiation`:
119
+
120
+ ```tsx
121
+ <BottomSheet
122
+ index={index}
123
+ onIndexChange={setIndex}
124
+ disableScrollableNegotiation
125
+ >
126
+ {/* ... */}
127
+ </BottomSheet>
128
+ ```
129
+
109
130
  ### Detents and index
110
131
 
111
132
  Detents are the points to which the sheet snaps. Each detent is either a number
@@ -139,6 +160,9 @@ const [index, setIndex] = useState(0);
139
160
  </BottomSheet>
140
161
  ```
141
162
 
163
+ Detents can also change over time. When you update `detents`, the sheet keeps
164
+ the current index and animates to the updated detent height when needed.
165
+
142
166
  #### Programmatic-only detents
143
167
 
144
168
  If you want a detent to be reachable only via code (not by dragging), use the
@@ -83,6 +83,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
83
83
  private var scrimTouchActive = false
84
84
  private var scrimColor = Color.TRANSPARENT
85
85
  private var scrimProgress = 0f
86
+ private var suppressScrimForClosingTarget = false
86
87
  private var maxDetentHeight = Float.NaN
87
88
  private var contentHeightMarker: View? = null
88
89
 
@@ -146,10 +147,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
146
147
 
147
148
  // MARK: - Layout
148
149
 
149
- override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
150
- super.onLayout(changed, l, t, r, b)
151
- val w = r - l
152
- val h = b - t
150
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
151
+ super.onLayout(changed, left, top, right, bottom)
152
+ val w = right - left
153
+ val h = bottom - top
153
154
  if (w <= 0 || h <= 0) return
154
155
 
155
156
  refreshContentHeightMarker()
@@ -157,10 +158,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
157
158
  layoutSheetContainer(w, h)
158
159
 
159
160
  if (!hasLaidOut && detentSpecs.isNotEmpty()) {
160
- hasLaidOut = true
161
161
  val indexToApply = pendingIndex ?: targetIndex
162
+ val clampedIndex = indexToApply.coerceIn(0, detentSpecs.size - 1)
163
+
164
+ if (animateIn && isInvalidContentDetentTarget(clampedIndex)) {
165
+ targetIndex = clampedIndex
166
+ pendingIndex = clampedIndex
167
+ val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
168
+ sheetContainer.translationY = closedTy
169
+ emitPosition()
170
+ return
171
+ }
172
+
173
+ hasLaidOut = true
162
174
  pendingIndex = null
163
- targetIndex = indexToApply.coerceIn(0, detentSpecs.size - 1)
175
+ targetIndex = clampedIndex
164
176
 
165
177
  if (animateIn) {
166
178
  val closedTy = detentSpecs.maxOfOrNull { it.height } ?: h.toFloat()
@@ -248,7 +260,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
248
260
 
249
261
  private fun resolveDetentSpecs(): List<DetentSpec> {
250
262
  val maxHeight = resolvedMaxDetentHeight()
251
- val contentHeight = currentContentHeight().takeIf { it.isFinite() && it > 0f } ?: maxHeight
263
+ val contentHeight = validContentHeight().takeIf { it.isFinite() } ?: maxHeight
252
264
  return rawDetentSpecs.map { spec ->
253
265
  val height =
254
266
  when (spec.kind) {
@@ -260,6 +272,11 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
260
272
  }
261
273
 
262
274
  private fun refreshDetentsFromLayout() {
275
+ if (hasLaidOut && isInvalidContentDetentTarget(targetIndex)) {
276
+ updateScrim()
277
+ return
278
+ }
279
+
263
280
  val resolvedDetents = resolveDetentSpecs()
264
281
  if (resolvedDetents == detentSpecs) {
265
282
  updateScrim()
@@ -272,6 +289,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
272
289
 
273
290
  if (hasLaidOut && !isPanning) {
274
291
  targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
292
+ if (activeAnimation != null && isTargetingClosedDetent) {
293
+ suppressScrimForClosingTarget = true
294
+ hideScrim()
295
+ }
275
296
  if (activeAnimation != null) {
276
297
  val currentTy = sheetContainer.translationY
277
298
  val shouldEmitSettle = activeAnimationEmitsSettle
@@ -305,6 +326,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
305
326
  return marker.top.toFloat()
306
327
  }
307
328
 
329
+ private fun validContentHeight(): Float {
330
+ return currentContentHeight().takeIf { it.isFinite() && it > 0f } ?: Float.NaN
331
+ }
332
+
333
+ private fun isInvalidContentDetentTarget(index: Int): Boolean {
334
+ return rawDetentSpecs.getOrNull(index)?.kind == DetentKind.CONTENT &&
335
+ !validContentHeight().isFinite()
336
+ }
337
+
308
338
  private fun refreshContentHeightMarker() {
309
339
  val marker = findContentHeightMarker()
310
340
  if (marker === contentHeightMarker) return
@@ -339,6 +369,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
339
369
  private val firstNonZeroDetentHeight: Float
340
370
  get() = detentSpecs.firstOrNull { it.height > 0f }?.height ?: 0f
341
371
 
372
+ private val isTargetingClosedDetent: Boolean
373
+ get() = closedIndex?.let { targetIndex == it } == true
374
+
342
375
  private val draggableMinTy: Float
343
376
  get() {
344
377
  val highestIndex = detentSpecs.indices.lastOrNull { !detentSpecs[it].programmatic } ?: 0
@@ -413,6 +446,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
413
446
  ) {
414
447
  if (index < 0 || index >= detentSpecs.size) return
415
448
  targetIndex = index
449
+ if (!isTargetingClosedDetent) {
450
+ suppressScrimForClosingTarget = false
451
+ }
416
452
 
417
453
  val targetTy = translationY(index)
418
454
  activeAnimationEmitsSettle = emitSettle
@@ -433,9 +469,14 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
433
469
  return@addEndListener
434
470
  }
435
471
  stopChoreographer()
436
- emitPosition()
437
472
  activeAnimation = null
438
473
  activeAnimationEmitsSettle = false
474
+ suppressScrimForClosingTarget = false
475
+ if (closedIndex == index) {
476
+ sheetContainer.translationY = translationY(index)
477
+ hideScrim()
478
+ }
479
+ emitPosition()
439
480
  updateInteractionState()
440
481
  if (emitIndexChange) listener?.onIndexChange(index)
441
482
  if (emitSettle) listener?.onSettle(index)
@@ -469,14 +510,14 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
469
510
 
470
511
  // MARK: - Touch handling
471
512
 
472
- override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
513
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
473
514
  val sheetTop = sheetContainer.top + sheetContainer.translationY
474
- if (ev.actionMasked == MotionEvent.ACTION_DOWN && ev.y < sheetTop) {
515
+ if (event.actionMasked == MotionEvent.ACTION_DOWN && event.y < sheetTop) {
475
516
  if (isScrimVisible()) {
476
- initialTouchX = ev.x
477
- initialTouchY = ev.y
478
- lastTouchY = ev.y
479
- activePointerId = ev.getPointerId(0)
517
+ initialTouchX = event.x
518
+ initialTouchY = event.y
519
+ lastTouchY = event.y
520
+ activePointerId = event.getPointerId(0)
480
521
  scrimPressed = true
481
522
  scrimTouchActive = true
482
523
  return true
@@ -484,19 +525,19 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
484
525
  return false
485
526
  }
486
527
 
487
- when (ev.actionMasked) {
528
+ when (event.actionMasked) {
488
529
  MotionEvent.ACTION_DOWN -> {
489
- initialTouchX = ev.x
490
- initialTouchY = ev.y
491
- lastTouchY = ev.y
492
- activePointerId = ev.getPointerId(0)
530
+ initialTouchX = event.x
531
+ initialTouchY = event.y
532
+ lastTouchY = event.y
533
+ activePointerId = event.getPointerId(0)
493
534
  }
494
535
  MotionEvent.ACTION_MOVE -> {
495
536
  if (activePointerId == MotionEvent.INVALID_POINTER_ID) return false
496
- val pointerIndex = ev.findPointerIndex(activePointerId)
537
+ val pointerIndex = event.findPointerIndex(activePointerId)
497
538
  if (pointerIndex < 0) return false
498
- val x = ev.getX(pointerIndex)
499
- val y = ev.getY(pointerIndex)
539
+ val x = event.getX(pointerIndex)
540
+ val y = event.getY(pointerIndex)
500
541
  val dx = x - initialTouchX
501
542
  val dy = y - initialTouchY
502
543
 
@@ -511,13 +552,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
511
552
  // processes events at the root view level before onInterceptTouchEvent
512
553
  // runs, so without this the JS side never sees a cancel and Pressable
513
554
  // would still fire onPress.
514
- NativeGestureUtil.notifyNativeGestureStarted(this, ev)
555
+ NativeGestureUtil.notifyNativeGestureStarted(this, event)
515
556
  return true
516
557
  }
517
558
  if (dy > 0 && isScrollViewAtTop()) {
518
559
  lastTouchY = y
519
560
  requestDisallowInterceptTouchEvent(false)
520
- NativeGestureUtil.notifyNativeGestureStarted(this, ev)
561
+ NativeGestureUtil.notifyNativeGestureStarted(this, event)
521
562
  return true
522
563
  }
523
564
  }
@@ -716,6 +757,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
716
757
  scrimTouchActive = false
717
758
  sheetContainer.translationY = 0f
718
759
  scrimProgress = 0f
760
+ suppressScrimForClosingTarget = false
719
761
  sheetContainer.removeAllViews()
720
762
  stateWrapper = null
721
763
  lastShadowOffsetY = Float.NaN
@@ -731,10 +773,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
731
773
  // When settled at the closed detent, dynamic content updates can briefly
732
774
  // produce stale non-zero positions. Keep scrim hidden in this state.
733
775
  if (
734
- closedIndex != null && targetIndex == closedIndex && activeAnimation == null && !isPanning
776
+ (isTargetingClosedDetent && activeAnimation == null && !isPanning) ||
777
+ (suppressScrimForClosingTarget && isTargetingClosedDetent)
735
778
  ) {
736
- scrimProgress = 0f
737
- invalidate()
779
+ hideScrim()
738
780
  return
739
781
  }
740
782
 
@@ -743,6 +785,11 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
743
785
  invalidate()
744
786
  }
745
787
 
788
+ private fun hideScrim() {
789
+ scrimProgress = 0f
790
+ invalidate()
791
+ }
792
+
746
793
  private fun updateInteractionState() {
747
794
  pointerEvents =
748
795
  if (modal && (activeAnimation != null || isPanning || isScrimVisible())) {
@@ -127,10 +127,21 @@ public final class RNSBottomSheetHostingView: UIView {
127
127
  sheetContainer.center = CGPoint(x: bounds.width / 2, y: bounds.height - maxHeight / 2)
128
128
 
129
129
  if !hasLaidOut && !detentSpecs.isEmpty {
130
- hasLaidOut = true
131
130
  let indexToApply = pendingIndex ?? targetIndex
131
+ let clampedIndex = max(0, min(detentSpecs.count - 1, indexToApply))
132
+
133
+ if animateIn && isInvalidContentDetentTarget(clampedIndex) {
134
+ targetIndex = clampedIndex
135
+ pendingIndex = clampedIndex
136
+ let closedTy = maximumResolvedDetentHeight ?? bounds.height
137
+ sheetContainer.transform = CGAffineTransform(translationX: 0, y: closedTy)
138
+ emitPosition()
139
+ return
140
+ }
141
+
142
+ hasLaidOut = true
132
143
  pendingIndex = nil
133
- targetIndex = max(0, min(detentSpecs.count - 1, indexToApply))
144
+ targetIndex = clampedIndex
134
145
 
135
146
  if animateIn {
136
147
  let closedTy = maximumResolvedDetentHeight ?? bounds.height
@@ -529,7 +540,7 @@ public final class RNSBottomSheetHostingView: UIView {
529
540
 
530
541
  private func resolveDetentSpecs() -> [DetentSpec] {
531
542
  let maxHeight = resolvedMaxDetentHeight
532
- let contentHeight = currentContentHeight.map { min($0, maxHeight) } ?? maxHeight
543
+ let contentHeight = validContentHeight.map { min($0, maxHeight) } ?? maxHeight
533
544
  return rawDetentSpecs.map { spec in
534
545
  let height: CGFloat
535
546
  switch spec.kind {
@@ -544,6 +555,11 @@ public final class RNSBottomSheetHostingView: UIView {
544
555
 
545
556
  private func refreshDetentsFromLayout() {
546
557
  refreshContentHeightMarker()
558
+ if hasLaidOut, isInvalidContentDetentTarget(targetIndex) {
559
+ updateScrim()
560
+ return
561
+ }
562
+
547
563
  let resolvedDetents = resolveDetentSpecs()
548
564
  guard resolvedDetents != detentSpecs else {
549
565
  updateScrim()
@@ -598,6 +614,25 @@ public final class RNSBottomSheetHostingView: UIView {
598
614
  guard let marker = contentHeightMarker else { return nil }
599
615
  return marker.frame.minY.isFinite ? marker.frame.minY : nil
600
616
  }
617
+
618
+ private var validContentHeight: CGFloat? {
619
+ guard let height = currentContentHeight, height.isFinite, height > 0 else {
620
+ return nil
621
+ }
622
+ return height
623
+ }
624
+
625
+ private func isInvalidContentDetentTarget(_ index: Int) -> Bool {
626
+ guard rawDetentSpecs.indices.contains(index) else {
627
+ return false
628
+ }
629
+ switch rawDetentSpecs[index].kind {
630
+ case .points:
631
+ return false
632
+ case .content:
633
+ return validContentHeight == nil
634
+ }
635
+ }
601
636
  }
602
637
 
603
638
  extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
@@ -1,16 +1,13 @@
1
1
  "use strict";
2
2
 
3
- import { createContext, useContext, useEffect, useId, useReducer, useState } from 'react';
3
+ import { createContext, useContext, useId, useLayoutEffect, useState, useSyncExternalStore } from 'react';
4
4
  import { StyleSheet, View } from 'react-native';
5
5
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  const PortalContext = /*#__PURE__*/createContext(null);
7
7
  const PortalHost = () => {
8
8
  const context = useContext(PortalContext);
9
- const [, forceRender] = useReducer(x => x + 1, 0);
10
- useEffect(() => {
11
- return context.subscribe(forceRender);
12
- }, [context]);
13
- return Array.from(context.getPortals().entries()).map(([key, element]) => /*#__PURE__*/_jsx(View, {
9
+ const portals = useSyncExternalStore(context.subscribe, context.getSnapshot, context.getSnapshot);
10
+ return portals.map(([key, element]) => /*#__PURE__*/_jsx(View, {
14
11
  style: StyleSheet.absoluteFill,
15
12
  pointerEvents: "box-none",
16
13
  children: element
@@ -24,7 +21,9 @@ export const BottomSheetProvider = ({
24
21
  const [context] = useState(() => {
25
22
  const portals = new Map();
26
23
  const subscribers = new Set();
24
+ let snapshot = [];
27
25
  const notify = () => {
26
+ snapshot = Array.from(portals.entries());
28
27
  subscribers.forEach(subscriber => subscriber());
29
28
  };
30
29
  return {
@@ -42,7 +41,7 @@ export const BottomSheetProvider = ({
42
41
  subscribers.delete(callback);
43
42
  };
44
43
  },
45
- getPortals: () => portals
44
+ getSnapshot: () => snapshot
46
45
  };
47
46
  });
48
47
  return /*#__PURE__*/_jsxs(PortalContext.Provider, {
@@ -62,10 +61,10 @@ export const Portal = ({
62
61
  removePortal
63
62
  } = context;
64
63
  const id = useId();
65
- useEffect(() => {
64
+ useLayoutEffect(() => {
66
65
  addPortal(id, children);
67
66
  }, [id, children, addPortal]);
68
- useEffect(() => {
67
+ useLayoutEffect(() => {
69
68
  return () => {
70
69
  removePortal(id);
71
70
  };
@@ -1 +1 @@
1
- {"version":3,"names":["createContext","useContext","useEffect","useId","useReducer","useState","StyleSheet","View","jsx","_jsx","jsxs","_jsxs","PortalContext","PortalHost","context","forceRender","x","subscribe","Array","from","getPortals","entries","map","key","element","style","absoluteFill","pointerEvents","children","BottomSheetProvider","portals","Map","subscribers","Set","notify","forEach","subscriber","addPortal","set","removePortal","delete","callback","add","Provider","value","Portal","Error","id"],"sourceRoot":"../../src","sources":["BottomSheetProvider.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["createContext","useContext","useId","useLayoutEffect","useState","useSyncExternalStore","StyleSheet","View","jsx","_jsx","jsxs","_jsxs","PortalContext","PortalHost","context","portals","subscribe","getSnapshot","map","key","element","style","absoluteFill","pointerEvents","children","BottomSheetProvider","Map","subscribers","Set","snapshot","notify","Array","from","entries","forEach","subscriber","addPortal","set","removePortal","delete","callback","add","Provider","value","Portal","Error","id"],"sourceRoot":"../../src","sources":["BottomSheetProvider.tsx"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA0BvC,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CAgCxE,CAAC;AAEF,eAAO,MAAM,MAAM,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,SAkB3D,CAAC"}
1
+ {"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA6BvC,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CAkCxE,CAAC;AAEF,eAAO,MAAM,MAAM,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,SAkB3D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-next.1",
4
4
  "description": "Provides bottom-sheet components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,31 +1,34 @@
1
1
  import {
2
2
  createContext,
3
3
  useContext,
4
- useEffect,
5
4
  useId,
6
- useReducer,
5
+ useLayoutEffect,
7
6
  useState,
7
+ useSyncExternalStore,
8
8
  } from 'react';
9
9
  import type { ReactNode } from 'react';
10
10
  import { StyleSheet, View } from 'react-native';
11
11
 
12
+ type PortalSnapshot = Array<[string, ReactNode]>;
13
+
12
14
  interface PortalContextType {
13
15
  addPortal: (key: string, element: ReactNode) => void;
14
16
  removePortal: (key: string) => void;
15
17
  subscribe: (callback: () => void) => () => void;
16
- getPortals: () => Map<string, ReactNode>;
18
+ getSnapshot: () => PortalSnapshot;
17
19
  }
18
20
 
19
21
  const PortalContext = createContext<PortalContextType | null>(null);
20
22
 
21
23
  const PortalHost = () => {
22
24
  const context = useContext(PortalContext)!;
23
- const [, forceRender] = useReducer((x: number) => x + 1, 0);
24
- useEffect(() => {
25
- return context.subscribe(forceRender);
26
- }, [context]);
25
+ const portals = useSyncExternalStore(
26
+ context.subscribe,
27
+ context.getSnapshot,
28
+ context.getSnapshot
29
+ );
27
30
 
28
- return Array.from(context.getPortals().entries()).map(([key, element]) => (
31
+ return portals.map(([key, element]) => (
29
32
  <View key={key} style={StyleSheet.absoluteFill} pointerEvents="box-none">
30
33
  {element}
31
34
  </View>
@@ -37,7 +40,9 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
37
40
  const [context] = useState<PortalContextType>(() => {
38
41
  const portals = new Map<string, ReactNode>();
39
42
  const subscribers = new Set<() => void>();
43
+ let snapshot: PortalSnapshot = [];
40
44
  const notify = () => {
45
+ snapshot = Array.from(portals.entries());
41
46
  subscribers.forEach((subscriber) => subscriber());
42
47
  };
43
48
  return {
@@ -55,7 +60,7 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
55
60
  subscribers.delete(callback);
56
61
  };
57
62
  },
58
- getPortals: () => portals,
63
+ getSnapshot: () => snapshot,
59
64
  };
60
65
  });
61
66
 
@@ -76,10 +81,10 @@ export const Portal = ({ children }: { children: ReactNode }) => {
76
81
  const { addPortal, removePortal } = context;
77
82
  const id = useId();
78
83
 
79
- useEffect(() => {
84
+ useLayoutEffect(() => {
80
85
  addPortal(id, children);
81
86
  }, [id, children, addPortal]);
82
- useEffect(() => {
87
+ useLayoutEffect(() => {
83
88
  return () => {
84
89
  removePortal(id);
85
90
  };