react-native-puff-pop 1.0.2 → 1.0.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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useCallback, } from 'react';
2
+ import { useEffect, useRef, useState, useCallback, useMemo, } from 'react';
3
3
  import { View, Animated, StyleSheet, Easing, } from 'react-native';
4
4
  /**
5
5
  * Get easing function based on type
@@ -37,6 +37,25 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
37
37
  const animatedHeight = useRef(new Animated.Value(0)).current;
38
38
  const hasAnimated = useRef(false);
39
39
  const loopAnimationRef = useRef(null);
40
+ const loopTimeoutRef = useRef(null);
41
+ // Memoize effect type checks to avoid repeated includes() calls
42
+ const effectFlags = useMemo(() => ({
43
+ hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
44
+ hasRotate: ['rotate', 'rotateScale'].includes(effect),
45
+ hasFlip: effect === 'flip',
46
+ hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
47
+ hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
48
+ hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
49
+ }), [effect]);
50
+ // Memoize interpolations to avoid recreating on every render
51
+ const rotateInterpolation = useMemo(() => rotate.interpolate({
52
+ inputRange: [-360, 0, 360],
53
+ outputRange: ['-360deg', '0deg', '360deg'],
54
+ }), [rotate]);
55
+ const flipInterpolation = useMemo(() => rotate.interpolate({
56
+ inputRange: [-180, 0],
57
+ outputRange: ['-180deg', '0deg'],
58
+ }), [rotate]);
40
59
  // Handle layout measurement for non-skeleton mode
41
60
  const onLayout = useCallback((event) => {
42
61
  if (!skeleton && measuredHeight === null) {
@@ -62,7 +81,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
62
81
  ...config,
63
82
  }));
64
83
  // Scale animation
65
- if (['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect)) {
84
+ if (effectFlags.hasScale) {
66
85
  const targetScale = toVisible ? 1 : getInitialScale(effect);
67
86
  animations.push(Animated.timing(scale, {
68
87
  toValue: targetScale,
@@ -71,7 +90,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
71
90
  }));
72
91
  }
73
92
  // Rotate animation
74
- if (['rotate', 'rotateScale', 'flip'].includes(effect)) {
93
+ if (effectFlags.hasRotate || effectFlags.hasFlip) {
75
94
  const targetRotate = toVisible ? 0 : getInitialRotate(effect);
76
95
  animations.push(Animated.timing(rotate, {
77
96
  toValue: targetRotate,
@@ -79,7 +98,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
79
98
  }));
80
99
  }
81
100
  // TranslateX animation
82
- if (['slideLeft', 'slideRight'].includes(effect)) {
101
+ if (effectFlags.hasTranslateX) {
83
102
  const targetX = toVisible ? 0 : getInitialTranslateX(effect);
84
103
  animations.push(Animated.timing(translateX, {
85
104
  toValue: targetX,
@@ -87,7 +106,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
87
106
  }));
88
107
  }
89
108
  // TranslateY animation
90
- if (['slideUp', 'slideDown', 'bounce'].includes(effect)) {
109
+ if (effectFlags.hasTranslateY) {
91
110
  const targetY = toVisible ? 0 : getInitialTranslateY(effect);
92
111
  animations.push(Animated.timing(translateY, {
93
112
  toValue: targetY,
@@ -144,7 +163,11 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
144
163
  if (loopCount === -1 || currentIteration < loopCount) {
145
164
  // Add delay between loops if specified
146
165
  if (loopDelay > 0) {
147
- setTimeout(runLoop, loopDelay);
166
+ // Clear any existing timeout before setting a new one
167
+ if (loopTimeoutRef.current) {
168
+ clearTimeout(loopTimeoutRef.current);
169
+ }
170
+ loopTimeoutRef.current = setTimeout(runLoop, loopDelay);
148
171
  }
149
172
  else {
150
173
  runLoop();
@@ -176,6 +199,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
176
199
  duration,
177
200
  easing,
178
201
  effect,
202
+ effectFlags,
179
203
  measuredHeight,
180
204
  onAnimationComplete,
181
205
  opacity,
@@ -201,44 +225,30 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
201
225
  animate(visible);
202
226
  }
203
227
  }, [visible, animate]);
204
- // Cleanup loop animation on unmount
228
+ // Cleanup loop animation and timeout on unmount
205
229
  useEffect(() => {
206
230
  return () => {
207
231
  if (loopAnimationRef.current) {
208
232
  loopAnimationRef.current.stop();
209
233
  }
234
+ if (loopTimeoutRef.current) {
235
+ clearTimeout(loopTimeoutRef.current);
236
+ }
210
237
  };
211
238
  }, []);
212
- // For non-skeleton mode, measure first
213
- if (!skeleton && measuredHeight === null) {
214
- return (_jsx(View, { style: styles.measureContainer, onLayout: onLayout, children: _jsx(View, { style: styles.hidden, children: children }) }));
215
- }
216
- // Build transform based on effect
217
- const getTransform = () => {
218
- const hasScale = ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect);
219
- const hasRotate = ['rotate', 'rotateScale'].includes(effect);
220
- const hasFlip = effect === 'flip';
221
- const hasTranslateX = ['slideLeft', 'slideRight'].includes(effect);
222
- const hasTranslateY = ['slideUp', 'slideDown', 'bounce'].includes(effect);
239
+ // Memoize transform array to avoid recreating on every render
240
+ // IMPORTANT: All hooks must be called before any conditional returns
241
+ const transform = useMemo(() => {
242
+ const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
223
243
  const transforms = [];
224
244
  if (hasScale) {
225
245
  transforms.push({ scale });
226
246
  }
227
247
  if (hasRotate) {
228
- transforms.push({
229
- rotate: rotate.interpolate({
230
- inputRange: [-360, 0, 360],
231
- outputRange: ['-360deg', '0deg', '360deg'],
232
- }),
233
- });
248
+ transforms.push({ rotate: rotateInterpolation });
234
249
  }
235
250
  if (hasFlip) {
236
- transforms.push({
237
- rotateY: rotate.interpolate({
238
- inputRange: [-180, 0],
239
- outputRange: ['-180deg', '0deg'],
240
- }),
241
- });
251
+ transforms.push({ rotateY: flipInterpolation });
242
252
  }
243
253
  if (hasTranslateX) {
244
254
  transforms.push({ translateX });
@@ -247,15 +257,26 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
247
257
  transforms.push({ translateY });
248
258
  }
249
259
  return transforms.length > 0 ? transforms : undefined;
250
- };
251
- const animatedStyle = {
260
+ }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
261
+ // Memoize animated style
262
+ const animatedStyle = useMemo(() => ({
252
263
  opacity,
253
- transform: getTransform(),
254
- };
255
- // Container style for non-skeleton mode
256
- const containerAnimatedStyle = !skeleton && measuredHeight !== null
257
- ? { height: animatedHeight, overflow: 'hidden' }
258
- : {};
264
+ transform,
265
+ }), [opacity, transform]);
266
+ // Memoize container style for non-skeleton mode
267
+ const containerAnimatedStyle = useMemo(() => {
268
+ if (!skeleton && measuredHeight !== null) {
269
+ return {
270
+ height: animatedHeight,
271
+ overflow: effectFlags.hasRotateEffect ? 'visible' : 'hidden'
272
+ };
273
+ }
274
+ return {};
275
+ }, [skeleton, measuredHeight, animatedHeight, effectFlags.hasRotateEffect]);
276
+ // For non-skeleton mode, measure first (after all hooks)
277
+ if (!skeleton && measuredHeight === null) {
278
+ return (_jsx(View, { style: styles.measureContainer, onLayout: onLayout, children: _jsx(View, { style: styles.hidden, children: children }) }));
279
+ }
259
280
  return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
260
281
  }
261
282
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-puff-pop",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
package/src/index.tsx CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  useRef,
4
4
  useState,
5
5
  useCallback,
6
+ useMemo,
6
7
  type ReactNode,
7
8
  type ReactElement,
8
9
  } from 'react';
@@ -168,6 +169,30 @@ export function PuffPop({
168
169
  const animatedHeight = useRef(new Animated.Value(0)).current;
169
170
  const hasAnimated = useRef(false);
170
171
  const loopAnimationRef = useRef<Animated.CompositeAnimation | null>(null);
172
+ const loopTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
173
+
174
+ // Memoize effect type checks to avoid repeated includes() calls
175
+ const effectFlags = useMemo(() => ({
176
+ hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
177
+ hasRotate: ['rotate', 'rotateScale'].includes(effect),
178
+ hasFlip: effect === 'flip',
179
+ hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
180
+ hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
181
+ hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
182
+ }), [effect]);
183
+
184
+ // Memoize interpolations to avoid recreating on every render
185
+ const rotateInterpolation = useMemo(() =>
186
+ rotate.interpolate({
187
+ inputRange: [-360, 0, 360],
188
+ outputRange: ['-360deg', '0deg', '360deg'],
189
+ }), [rotate]);
190
+
191
+ const flipInterpolation = useMemo(() =>
192
+ rotate.interpolate({
193
+ inputRange: [-180, 0],
194
+ outputRange: ['-180deg', '0deg'],
195
+ }), [rotate]);
171
196
 
172
197
  // Handle layout measurement for non-skeleton mode
173
198
  const onLayout = useCallback(
@@ -204,7 +229,7 @@ export function PuffPop({
204
229
  );
205
230
 
206
231
  // Scale animation
207
- if (['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect)) {
232
+ if (effectFlags.hasScale) {
208
233
  const targetScale = toVisible ? 1 : getInitialScale(effect);
209
234
  animations.push(
210
235
  Animated.timing(scale, {
@@ -216,7 +241,7 @@ export function PuffPop({
216
241
  }
217
242
 
218
243
  // Rotate animation
219
- if (['rotate', 'rotateScale', 'flip'].includes(effect)) {
244
+ if (effectFlags.hasRotate || effectFlags.hasFlip) {
220
245
  const targetRotate = toVisible ? 0 : getInitialRotate(effect);
221
246
  animations.push(
222
247
  Animated.timing(rotate, {
@@ -227,7 +252,7 @@ export function PuffPop({
227
252
  }
228
253
 
229
254
  // TranslateX animation
230
- if (['slideLeft', 'slideRight'].includes(effect)) {
255
+ if (effectFlags.hasTranslateX) {
231
256
  const targetX = toVisible ? 0 : getInitialTranslateX(effect);
232
257
  animations.push(
233
258
  Animated.timing(translateX, {
@@ -238,7 +263,7 @@ export function PuffPop({
238
263
  }
239
264
 
240
265
  // TranslateY animation
241
- if (['slideUp', 'slideDown', 'bounce'].includes(effect)) {
266
+ if (effectFlags.hasTranslateY) {
242
267
  const targetY = toVisible ? 0 : getInitialTranslateY(effect);
243
268
  animations.push(
244
269
  Animated.timing(translateY, {
@@ -306,7 +331,11 @@ export function PuffPop({
306
331
  if (loopCount === -1 || currentIteration < loopCount) {
307
332
  // Add delay between loops if specified
308
333
  if (loopDelay > 0) {
309
- setTimeout(runLoop, loopDelay);
334
+ // Clear any existing timeout before setting a new one
335
+ if (loopTimeoutRef.current) {
336
+ clearTimeout(loopTimeoutRef.current);
337
+ }
338
+ loopTimeoutRef.current = setTimeout(runLoop, loopDelay);
310
339
  } else {
311
340
  runLoop();
312
341
  }
@@ -338,6 +367,7 @@ export function PuffPop({
338
367
  duration,
339
368
  easing,
340
369
  effect,
370
+ effectFlags,
341
371
  measuredHeight,
342
372
  onAnimationComplete,
343
373
  opacity,
@@ -367,32 +397,22 @@ export function PuffPop({
367
397
  }
368
398
  }, [visible, animate]);
369
399
 
370
- // Cleanup loop animation on unmount
400
+ // Cleanup loop animation and timeout on unmount
371
401
  useEffect(() => {
372
402
  return () => {
373
403
  if (loopAnimationRef.current) {
374
404
  loopAnimationRef.current.stop();
375
405
  }
406
+ if (loopTimeoutRef.current) {
407
+ clearTimeout(loopTimeoutRef.current);
408
+ }
376
409
  };
377
410
  }, []);
378
411
 
379
- // For non-skeleton mode, measure first
380
- if (!skeleton && measuredHeight === null) {
381
- return (
382
- <View style={styles.measureContainer} onLayout={onLayout}>
383
- <View style={styles.hidden}>{children}</View>
384
- </View>
385
- );
386
- }
387
-
388
- // Build transform based on effect
389
- const getTransform = () => {
390
- const hasScale = ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect);
391
- const hasRotate = ['rotate', 'rotateScale'].includes(effect);
392
- const hasFlip = effect === 'flip';
393
- const hasTranslateX = ['slideLeft', 'slideRight'].includes(effect);
394
- const hasTranslateY = ['slideUp', 'slideDown', 'bounce'].includes(effect);
395
-
412
+ // Memoize transform array to avoid recreating on every render
413
+ // IMPORTANT: All hooks must be called before any conditional returns
414
+ const transform = useMemo(() => {
415
+ const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
396
416
  const transforms = [];
397
417
 
398
418
  if (hasScale) {
@@ -400,21 +420,11 @@ export function PuffPop({
400
420
  }
401
421
 
402
422
  if (hasRotate) {
403
- transforms.push({
404
- rotate: rotate.interpolate({
405
- inputRange: [-360, 0, 360],
406
- outputRange: ['-360deg', '0deg', '360deg'],
407
- }),
408
- });
423
+ transforms.push({ rotate: rotateInterpolation });
409
424
  }
410
425
 
411
426
  if (hasFlip) {
412
- transforms.push({
413
- rotateY: rotate.interpolate({
414
- inputRange: [-180, 0],
415
- outputRange: ['-180deg', '0deg'],
416
- }),
417
- });
427
+ transforms.push({ rotateY: flipInterpolation });
418
428
  }
419
429
 
420
430
  if (hasTranslateX) {
@@ -426,17 +436,33 @@ export function PuffPop({
426
436
  }
427
437
 
428
438
  return transforms.length > 0 ? transforms : undefined;
429
- };
439
+ }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
430
440
 
431
- const animatedStyle = {
441
+ // Memoize animated style
442
+ const animatedStyle = useMemo(() => ({
432
443
  opacity,
433
- transform: getTransform(),
434
- };
444
+ transform,
445
+ }), [opacity, transform]);
446
+
447
+ // Memoize container style for non-skeleton mode
448
+ const containerAnimatedStyle = useMemo(() => {
449
+ if (!skeleton && measuredHeight !== null) {
450
+ return {
451
+ height: animatedHeight,
452
+ overflow: effectFlags.hasRotateEffect ? 'visible' as const : 'hidden' as const
453
+ };
454
+ }
455
+ return {};
456
+ }, [skeleton, measuredHeight, animatedHeight, effectFlags.hasRotateEffect]);
435
457
 
436
- // Container style for non-skeleton mode
437
- const containerAnimatedStyle = !skeleton && measuredHeight !== null
438
- ? { height: animatedHeight, overflow: 'hidden' as const }
439
- : {};
458
+ // For non-skeleton mode, measure first (after all hooks)
459
+ if (!skeleton && measuredHeight === null) {
460
+ return (
461
+ <View style={styles.measureContainer} onLayout={onLayout}>
462
+ <View style={styles.hidden}>{children}</View>
463
+ </View>
464
+ );
465
+ }
440
466
 
441
467
  return (
442
468
  <Animated.View style={[styles.container, style, containerAnimatedStyle]}>