Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius, style }) => {
const theme = useTheme();
const opacityAnim = useRef(new Animated.Value(0.3)).current;

useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(opacityAnim, {
toValue: 1,
duration: 700,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.3,
duration: 700,
useNativeDriver: true,
}),
])
);
loop.start();

return () => loop.stop();
}, [opacityAnim]);
Comment on lines +7 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'new Animated\.Value|Animated\.loop|useEffect' mobile/components/ui/Skeleton.js
rg -n -C2 '<Skeleton' mobile/screens/FriendsScreen.js
rg -n -C2 'Array\.from\(\{ length: 5 \}\)' mobile/screens/FriendsScreen.js

Repository: Devasy/splitwiser

Length of output: 1255


🏁 Script executed:

rg -n 'import.*Skeleton|<Skeleton' mobile/screens/ --type js

Repository: Devasy/splitwiser

Length of output: 537


Share animation state instead of starting one pulse loop per Skeleton instance.

Each mounted Skeleton starts its own infinite Animated.loop. In FriendsScreen's loading state, 5 rows render 3 Skeleton components each, resulting in 15 concurrent animation loops. This adds avoidable animation overhead and can impact smoothness on lower-end devices.

⚙️ Proposed refactor (allow shared animated value, keep local fallback)
-const Skeleton = ({ width, height, borderRadius, style }) => {
+const Skeleton = ({ width, height, borderRadius, style, animatedOpacity }) => {
   const theme = useTheme();
-  const opacityAnim = useRef(new Animated.Value(0.3)).current;
+  const localOpacityAnim = useRef(new Animated.Value(0.3)).current;
+  const opacityAnim = animatedOpacity ?? localOpacityAnim;

   useEffect(() => {
+    if (animatedOpacity) return undefined;
     const loop = Animated.loop(
       Animated.sequence([
         Animated.timing(opacityAnim, {
           toValue: 1,
           duration: 700,
           useNativeDriver: true,
         }),
         Animated.timing(opacityAnim, {
           toValue: 0.3,
           duration: 700,
           useNativeDriver: true,
         }),
       ])
     );
     loop.start();

     return () => loop.stop();
-  }, [opacityAnim]);
+  }, [animatedOpacity, localOpacityAnim]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` around lines 7 - 27, The Skeleton component
currently creates its own Animated.loop via the opacityAnim useRef and
useEffect, causing many concurrent loops; change Skeleton to accept an optional
shared animated value prop (e.g., sharedOpacityAnim) and, in the component
(identify opacityAnim and useEffect that calls
Animated.loop/loop.start/loop.stop), use the passed sharedOpacityAnim when
provided and only create the local Animated.Value and loop when no shared value
is supplied; ensure the shared loop is started/stopped in a single central place
(or lazily once in a module-level initializer) while keeping the current local
fallback behavior so existing usage still works.


return (
<Animated.View
style={[
{
width,
height,
borderRadius: borderRadius || 0,
backgroundColor: theme.colors.surfaceVariant,
opacity: opacityAnim,
},
style,
]}
/>
);
};

export default Skeleton;
58 changes: 6 additions & 52 deletions mobile/screens/FriendsScreen.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useIsFocused } from "@react-navigation/native";
import { useContext, useEffect, useRef, useState } from "react";
import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
Appbar,
Avatar,
Expand All @@ -12,6 +12,7 @@ import {
import HapticIconButton from '../components/ui/HapticIconButton';
import { HapticListAccordion } from '../components/ui/HapticList';
import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils';
import Skeleton from '../components/ui/Skeleton';
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";
Expand Down Expand Up @@ -167,42 +168,12 @@ const FriendsScreen = () => {
);
};

// Shimmer skeleton components
const opacityAnim = useRef(new Animated.Value(0.3)).current;
useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(opacityAnim, {
toValue: 1,
duration: 700,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.3,
duration: 700,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [opacityAnim]);

const SkeletonRow = () => (
<View style={styles.skeletonRow}>
<Animated.View
style={[styles.skeletonAvatar, { opacity: opacityAnim }]}
/>
<Skeleton width={48} height={48} borderRadius={24} />
<View style={{ flex: 1, marginLeft: 12 }}>
<Animated.View
style={[styles.skeletonLine, { width: "60%", opacity: opacityAnim }]}
/>
<Animated.View
style={[
styles.skeletonLineSmall,
{ width: "40%", opacity: opacityAnim },
]}
/>
<Skeleton width="60%" height={14} borderRadius={6} style={{ marginBottom: 6 }} />
<Skeleton width="40%" height={12} borderRadius={6} />
</View>
</View>
);
Expand Down Expand Up @@ -315,23 +286,6 @@ const styles = StyleSheet.create({
alignItems: "center",
marginBottom: 14,
},
skeletonAvatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: "#e0e0e0",
},
skeletonLine: {
height: 14,
backgroundColor: "#e0e0e0",
borderRadius: 6,
marginBottom: 6,
},
skeletonLineSmall: {
height: 12,
backgroundColor: "#e0e0e0",
borderRadius: 6,
},
});

export default FriendsScreen;
Loading