Skip to content

Commit

Permalink
Ensure the skeleton placeholder remains visible when the parent scree…
Browse files Browse the repository at this point in the history
…n’s state changes; Implement logic to reset the skeleton internally whenever the children change.
  • Loading branch information
Henrique Ramos committed Feb 12, 2025
1 parent a123dd8 commit 3ab4706
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 47 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-skeleton-placeholder",
"version": "6.0.0-beta.1",
"version": "6.0.0-beta.2",
"description": "SkeletonPlaceholder is a React Native library to easily create an amazing loading effect.",
"main": "lib/skeleton-placeholder.js",
"scripts": {
Expand Down Expand Up @@ -42,5 +42,6 @@
"react-native": ">=0.72.9",
"react-native-reanimated": "^3.15.1",
"react-native-svg": "^14.1.0"
}
},
"dependencies": {}
}
123 changes: 78 additions & 45 deletions src/skeleton-placeholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Defs, LinearGradient, Mask, Rect, Stop, Svg} from 'react-native-svg';

import SvgRenderer from './svg-rendered';
import {Measurements} from './types';
import flatten from './utils/flatten';

const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);

Expand Down Expand Up @@ -96,7 +97,23 @@ const SkeletonPlaceholder = ({
return measurements;
};

const renderRecursive = (node: React.ReactNode): React.ReactNode => {
const flattenedChildren = flatten(children as any);
const childrenStyles = flattenedChildren.map((child) => child.props.style);
/**
* TODO: find a better way to handle this;
* Every time a child changes, the hash changes and the component "re-renders".
* Then, the calculations are done again and the animation is reset.
*/
const hash = JSON.stringify(childrenStyles);

React.useEffect(() => {
return () => {
childRefs.current = [];
setContainerSize({width: 0, height: 0});
};
}, [hash]);

const renderRecursive = React.useCallback((node: React.ReactNode): React.ReactNode => {
return React.Children.map(
node,
(child: React.ReactElement<ViewProps & {ref: React.Ref<View>}>) => {
Expand All @@ -111,52 +128,68 @@ const SkeletonPlaceholder = ({
});
},
);
};
if (containerSize.width === 0 || containerSize.height === 0) {
}, []);

const MemoizedRender = React.useMemo(() => {
if (containerSize.width === 0 || containerSize.height === 0) {
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{position: 'absolute', zIndex: -1, opacity: 0}}
onLayout={(event) => {
setContainerSize({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
});
}}>
{renderRecursive(children as any)}
</View>
);
}

return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{position: 'absolute', zIndex: -1, opacity: 0}}
onLayout={(event) => {
setContainerSize({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
});
}}>
{renderRecursive(children as any)}
</View>
<Svg width={containerSize.width} height={containerSize.height}>
<Defs>
<AnimatedLinearGradient
id="grad"
gradientTransform={angle ? `rotate(${angle})` : undefined}
animatedProps={animatedProps}>
<Stop offset="0%" stopColor={backgroundColor} />
<Stop offset="50%" stopColor={highlightColor} />
<Stop offset="100%" stopColor={backgroundColor} />
</AnimatedLinearGradient>

<Mask id="shapeMask">
<SvgRenderer
defaultBorderRadius={borderRadius}
measurements={childRefs.current.map((_, index) => measureChild(index))}
/>
</Mask>
</Defs>
<Rect
x="0"
y="0"
width={containerSize.width}
height={containerSize.height}
fill="url(#grad)"
mask="url(#shapeMask)"
/>
</Svg>
);
}

return (
<Svg width={containerSize.width} height={containerSize.height}>
<Defs>
<AnimatedLinearGradient
id="grad"
gradientTransform={angle ? `rotate(${angle})` : undefined}
animatedProps={animatedProps}>
<Stop offset="0%" stopColor={backgroundColor} />
<Stop offset="50%" stopColor={highlightColor} />
<Stop offset="100%" stopColor={backgroundColor} />
</AnimatedLinearGradient>

<Mask id="shapeMask">
<SvgRenderer
defaultBorderRadius={borderRadius}
measurements={childRefs.current.map((_, index) => measureChild(index))}
/>
</Mask>
</Defs>
<Rect
x="0"
y="0"
width={containerSize.width}
height={containerSize.height}
fill="url(#grad)"
mask="url(#shapeMask)"
/>
</Svg>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
containerSize.width,
containerSize.height,
animatedProps,
angle,
backgroundColor,
highlightColor,
borderRadius,
childRefs,
renderRecursive,
]);

return MemoizedRender;
};

export default SkeletonPlaceholder;
13 changes: 13 additions & 0 deletions src/utils/flatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import {ViewProps} from 'react-native-svg/lib/typescript/fabric/utils';

const flatten = (children: React.ReactElement<ViewProps>, acc: any[] = []) => {
acc = [...acc, ...React.Children.toArray(children)];

if (children.props && children.props.children)
return flatten(children.props.children as React.ReactElement<ViewProps>, acc);

return acc;
};

export default flatten;

0 comments on commit 3ab4706

Please sign in to comment.