oihana-next-ui 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oihana-next-ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Oihana Next.js UI component library — reusable components, hooks and utilities built with React 19, Next.js, Tailwind CSS and DaisyUI",
6
6
  "author": {
@@ -5,7 +5,7 @@ import languages from './languages'
5
5
  import navigation from './navigation'
6
6
  import ui from './ui'
7
7
 
8
- import version from '@/version'
8
+ import version from '../version'
9
9
 
10
10
  const config =
11
11
  {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { COLLAPSE , LINK /* , DIVIDER */ } from '@/contexts/navigation/types'
3
+ import { COLLAPSE , LINK /* , DIVIDER */ } from '../contexts/navigation/types'
4
4
 
5
5
  import {
6
6
  MdOutlineSmartButton as ButtonIcon ,
@@ -1,4 +1,4 @@
1
- import version from '@/version';
1
+ import version from '../../version';
2
2
 
3
3
  const splashScreen =
4
4
  {
@@ -1,9 +1,9 @@
1
- import getLocaleFromI18n from '@/contexts/locale/helpers/getLocaleFromI18n' ;
1
+ import getLocaleFromI18n from '../contexts/locale/helpers/getLocaleFromI18n' ;
2
2
 
3
3
  import 'dayjs/locale/fr' ;
4
4
  import 'dayjs/locale/en' ;
5
5
 
6
- import languages from '@/@configs/languages' ;
6
+ import languages from '../@configs/languages' ;
7
7
 
8
8
  import app from './app' ;
9
9
  import components from './components' ;
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { useRef } from 'react' ;
4
4
 
5
- import Loading from '@/components/Loading' ;
5
+ import Loading from '../Loading' ;
6
6
 
7
- import useInfiniteScroll from '@/hooks/useInfiniteScroll' ;
7
+ import useInfiniteScroll from '../../hooks/useInfiniteScroll' ;
8
8
 
9
- import cn from '@/themes/helpers/cn' ;
9
+ import cn from '../../themes/helpers/cn' ;
10
10
 
11
11
  /**
12
12
  * Default root element type for the InfiniteScroll container.
@@ -1,6 +1,6 @@
1
1
  'use client' ;
2
2
 
3
- import { useCallback , useEffect , useState } from 'react' ;
3
+ import { useCallback , useEffect , useRef , useState } from 'react' ;
4
4
 
5
5
  import InfiniteScroll from '@/components/layouts/InfiniteScroll' ;
6
6
  import List from '@/components/lists/List' ;
@@ -192,15 +192,20 @@ const ChatPanel = () =>
192
192
  const [ loading , setLoading ] = useState( false ) ;
193
193
  const [ hasMore , setHasMore ] = useState( true ) ;
194
194
 
195
+ // Synchronous re-entrancy guard : blocks double loads from React StrictMode's
196
+ // double-invoke and from IntersectionObserver bursts, before any await.
197
+ const loadingRef = useRef( false ) ;
198
+
195
199
  // --------- Prepend the previous (older) page of messages
196
200
 
197
201
  const loadOlder = useCallback( async () =>
198
202
  {
199
- if ( loading || !hasMore )
203
+ if ( loadingRef.current || !hasMore )
200
204
  {
201
205
  return ;
202
206
  }
203
207
 
208
+ loadingRef.current = true ;
204
209
  setLoading( true ) ;
205
210
 
206
211
  await new Promise( resolve => setTimeout( resolve , LATENCY ) ) ;
@@ -212,8 +217,9 @@ const ChatPanel = () =>
212
217
  setLoaded( prev => [ ...older , ...prev ] ) ;
213
218
  setHasMore( start > 0 ) ;
214
219
  setLoading( false ) ;
220
+ loadingRef.current = false ;
215
221
  }
216
- , [ hasMore , loaded.length , loading ] ) ;
222
+ , [ hasMore , loaded.length ] ) ;
217
223
 
218
224
  // --------- Initial load (most recent page)
219
225
 
@@ -255,15 +261,20 @@ const InfiniteScrollDemo = () =>
255
261
  const [ loading , setLoading ] = useState( false ) ;
256
262
  const [ hasMore , setHasMore ] = useState( true ) ;
257
263
 
264
+ // Synchronous re-entrancy guard : blocks double loads from React StrictMode's
265
+ // double-invoke and from IntersectionObserver bursts, before any await.
266
+ const loadingRef = useRef( false ) ;
267
+
258
268
  // --------- Append the next page, guarding against concurrent loads
259
269
 
260
270
  const loadMore = useCallback( async () =>
261
271
  {
262
- if ( loading || !hasMore )
272
+ if ( loadingRef.current || !hasMore )
263
273
  {
264
274
  return ;
265
275
  }
266
276
 
277
+ loadingRef.current = true ;
267
278
  setLoading( true ) ;
268
279
 
269
280
  const { items : newItems , hasMore : more } = await fetchPage( page ) ;
@@ -272,8 +283,9 @@ const InfiniteScrollDemo = () =>
272
283
  setHasMore( more ) ;
273
284
  setPage( prev => prev + 1 ) ;
274
285
  setLoading( false ) ;
286
+ loadingRef.current = false ;
275
287
  }
276
- , [ hasMore , loading , page ] ) ;
288
+ , [ hasMore , page ] ) ;
277
289
 
278
290
  // --------- Initial load
279
291
 
@@ -21,11 +21,11 @@ import SplashScreen from './SplashScreen';
21
21
 
22
22
  import useVersionCheck from '../hooks/useVersionCheck'
23
23
 
24
- import config from '@/@configs'
25
- import languages from '@/@configs/languages'
26
- import locale from '@/@locale'
27
- import navigation from '@/@configs/navigation'
28
- import splashScreen from '@/@configs/ui/splashScreen' ;
24
+ import config from '../@configs'
25
+ import languages from '../@configs/languages'
26
+ import locale from '../@locale'
27
+ import navigation from '../@configs/navigation'
28
+ import splashScreen from '../@configs/ui/splashScreen' ;
29
29
 
30
30
  const { defaultLang , version , versionCheck } = config ;
31
31
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect , useRef } from 'react' ;
4
4
 
5
- import resolveRefElement from '@/helpers/react/resolveRefElement' ;
5
+ import resolveRefElement from '../helpers/react/resolveRefElement' ;
6
6
 
7
7
  /**
8
8
  * Default distance, before the sentinel is reached, at which loading is triggered.
@@ -28,6 +28,12 @@ export const DEFAULT_THRESHOLD = 0 ;
28
28
  * Note : `onLoadMore` should be stable (wrap it in `useCallback`), otherwise
29
29
  * the observer is recreated on every render.
30
30
  *
31
+ * Note : `onLoadMore` should also be re-entrancy safe. Because the `loading`
32
+ * state is asynchronous, React StrictMode's double-invoke and observer bursts
33
+ * can call it again before `loading` flips — guard with a synchronous ref
34
+ * (`if ( loadingRef.current ) return ; loadingRef.current = true ;`) to avoid
35
+ * loading the same page twice (which yields duplicate React keys).
36
+ *
31
37
  * @param {Object} [options]
32
38
  * @param {boolean} [options.hasMore=true] - Whether more items can be loaded.
33
39
  * @param {boolean} [options.loading=false] - Pauses observation while a load is in flight.
package/src/version.js CHANGED
@@ -1,3 +1,3 @@
1
- const version = "0.2.2" ;
1
+ const version = "0.2.3" ;
2
2
 
3
3
  export default version ;