You May Not Need Reanimated Measure cover with running horse silhouettes
React Native
You May Not Need Reanimated Measure
Maciej StosioMaciej StosioNov 24, 20255 min read

Let me take you on a short journey of implementing loading skeletons from scratch. You’ll see the problems I encountered, how I wanted to solve them with React Native Reanimated’s measure, and why it was wrong.

The other day I was faced with the task of replacing beloved ActivityIndicators with skeletons. Naturally, I assumed there must be a library that does that, but none of them met my requirements. After skimming through the implementations, I committed what every developer does from time to time— I decided to try building it myself.

The process started off pretty smoothly. I threw a masked view, new Expo’s experimental linear gradient and spiced it with some good old Reanimated:

const Skeleton = ({ style }: { style: ViewStyle}) => {
  const progress = useSharedValue(-100);

  useEffect(() => {
    progress.value = withRepeat(
      withSequence(
        withTiming(-100, { duration: 0 }),
        withTiming(100, { duration: 3000 })
      ),
      0
    );
  }, []);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: `${progress.value}%` }],
    };
  });

  return (
    <MaskedView
      style={style}
      maskElement={<View style={{ ...style, backgroundColor: "#000" }} />}
    >
      <Animated.View style={animatedStyle}>
        <View
          style={{
            ...style,
            experimental_backgroundImage: "linear-gradient(90deg, #001A72 45%, #6676aa 50%, #001A72 55%)",
            marginHorizontal: "-100%",
            width: "300%" ,
          }}
        />
      </Animated.View>
    </MaskedView>
  );
};

It looked pretty neat, until I used it to build something more complex:

Radon IDE simulator preview showing a placeholder profile layout with blue circular and rectangular skeleton blocks

So, we’re facing two issues: the gradients are different sizes, and the shimmer is supposed to flow across all the elements, not each one separately.

The first part was easy — I grabbed the device width using useWindowDimensions and replaced all the percentages with a constant value.

For the second issue, I figured it would be nice to get the element’s position on the screen and shift the animation accordingly. I couldn’t get it to work with the onLayout prop, so I checked the Reanimated documentation and found measure.

measure lets you synchronously get the dimensions and position of a view on the screen (…)

It worked like a charm, though I had to lean on the Elvis operator since measure needs to be used on rendered components. Otherwise, it just returns null.

Radon IDE simulator preview showing a profile placeholder layout with a blue gradient shimmer moving across the screen

const Skeleton = ({ style }: { style: ViewStyle}) => {
  const animatedRef = useAnimatedRef()
  const dimension = useWindowDimensions()
  const progress = useSharedValue(0);

  useEffect(() => {
    progress.value = withRepeat(
      withSequence(
        withTiming(-dimension.width, { duration: 0 }),
        withTiming(dimension.width, { duration: 3000 })
      ),
      -1
    );
  }, []);

  const animatedStyle = useAnimatedStyle(() => {
    const measured = measure(animatedRef)
    return {
      transform: [{ translateX: progress.value - (measured?.pageX ?? 0)}],
    };
  });

  return (
    <MaskedView
      style={style}
      maskElement={<View style={{ ...style, backgroundColor: "#000" }} />}
    >
      <Animated.View ref={animatedRef}>
        <Animated.View style={animatedStyle}>
          <View
            style={{
              ...style,
              experimental_backgroundImage: "linear-gradient(90deg, #001A72 45%, #6676aa 50%, #001A72 55%)",
              marginHorizontal: -dimension.width,
              width: 3 * dimension.width,
            }}
          />
        </Animated.View>
      </Animated.View>
    </MaskedView>
  );
};

The only thing was…

Console warning from Reanimated saying the view has an undefined or not-yet-computed LayoutMetrics value

“The view has some undefined, not-yet-computed or meaningless value (…)"

Yeah I know, thus the null-check…

After digging through the documentation and trying a few if hacks to work around the problem, I took the elevator to the open-source realm to vent about it.

They pulled the Reanimated source code, showed me where the warning came from, and shared the story that they had added it a while back — just waiting for someone to complain. Well, here I am.

After a quick brainstorming session, we realized I could use React Native’s built-in measure method. It works the same way but doesn’t need to run on every frame, which would be computationally expensive:

const [pageX, setPageX] = useState(0)

(...)

useLayoutEffect(() => {
  ref.current?.measure((_x, _y, _width, _height, pageX) => setPageX(pageX))
}, [])

const animatedStyle = useAnimatedStyle(() => {
  return {
    transform: [{ translateX: progress.value - pageX}],
  };
});

It worked just like before, but I no longer had to worry about null checks. By running it just once, I also saved some CPU. useLayoutEffect ensures everything is calculated before the user sees it. So… in the end, I didn’t need Reanimated’s measure!

The takeaway

Thanks for joining me on this journey. As we all know, Reanimated is a powerful tool, but we should keep in mind that some of its features should be used with caution. In my case, when I replaced Reanimated’s measure with the native one, I got a cleaner, faster solution without warnings.

Psst… Once I realized I didn’t need to use measure in useAnimatedStyle, I saw that I could switch to the new Reanimated CSS Animations:

import MaskedView from "@react-native-masked-view/masked-view";
import { useLayoutEffect, useRef, useState } from "react";
import { StyleSheet, Text, useWindowDimensions, View, ViewStyle } from "react-native";
import Animated from "react-native-reanimated";

const Skeleton = ({ style }: { style: ViewStyle}) => {
  const ref = useRef<View>(null)
  const [pageX, setPageX] = useState(0)
  const dimension = useWindowDimensions()

  useLayoutEffect(() => {
    ref.current?.measure((_x, _y, _width, _height, pageX) => setPageX(pageX))
  }, [])

  return (
    <MaskedView
      style={style}
      maskElement={<View style={{ ...style, backgroundColor: "#000" }} />}
    >
      <View ref={ref}>
        <Animated.View style={{
          animationName: {
            from: {
              transform: [
                {translateX: -dimension.width - pageX}
              ]
            },
            to: {
              transform: [
                {translateX: dimension.width - pageX}
              ]
            }
          },
          animationDuration: 3000,
          animationIterationCount: "infinite",
          animationTimingFunction: "ease",
        }}>
          <View
            style={{
              ...style,
              experimental_backgroundImage: "linear-gradient(90deg, #001A72 45%, #6676aa 50%, #001A72 55%)",
              marginHorizontal: -dimension.width,
              width: 3 * dimension.width,
            }}
          />
        </Animated.View>
      </View>
    </MaskedView>
  );
};

We’re Software Mansion: multimedia experts, AI explorers, React Native core contributors, community builders, and software development consultants.