Skip to content

Commit

Permalink
Break infinite queueMicrotask loop when updates are triggered in mapp…
Browse files Browse the repository at this point in the history
…er-run phase (#4358)

## Summary

This fixes a problem when reanimated would get stuck in an infinite
update loop triggered by shared value update in useAnimatedStyle or
useDerivedValue. Although we don't recommend updating shared values in
these stages of the mapper loop, it was still being used by some users
that way and therefore the change mapper logic we introduced in
reanimated 3 introduced a regression in that behavior.

When updating shared values in mapper phase we can never guarantee the
correct order of mapper execution (as it may be the case that some
mappers that used the shared value has already run and others didn't).
To match the behavior of reanimated 2 we enqueue an update for the next
frame that will run with the updated value.

## Test plan

I used the following example that updates height in useDerivedValue:
```ts

export default function App(): React.ReactElement {
  const randomWidth = useSharedValue(10);
  const randomHeight = useSharedValue(10);

  const config = {
    duration: 500,
    easing: Easing.bezierFn(0.5, 0.01, 0, 1),
  };

  const randomerWidth = useDerivedValue(() => {
    randomHeight.value = randomWidth.value * 0.5;
    return randomWidth.value * 0.5;
  });

  const style = useAnimatedStyle(() => {
    return {
      width: withTiming(randomerWidth.value, config),
      height: randomHeight.value,
    };
  });

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Animated.View
        style={[
          { width: 100, height: 80, backgroundColor: 'black', margin: 30 },
          style,
        ]}
      />
      <Button
        title="toggle"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
    </View>
  );
}
```

When pressing "toggle" button the old version would end up blocking UI
thread forever falling into an infinite loop of running microtasks. WIth
this update we allow the animation to process and you should see the
height and width change after each toggle press.

---------

Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
  • Loading branch information
kmagiera and tomekzaw authored Apr 18, 2023
1 parent 8953028 commit 7cdabc5
Showing 1 changed file with 19 additions and 1 deletion.
20 changes: 19 additions & 1 deletion src/reanimated2/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function createMapperRegistry() {
let sortedMappers: Mapper[] = [];

let runRequested = false;
let processingMappers = false;

function updateMappersOrder() {
// sort mappers topologically
Expand Down Expand Up @@ -79,6 +80,7 @@ export function createMapperRegistry() {
}

function mapperRun() {
processingMappers = true;
runRequested = false;
if (mappers.size !== sortedMappers.length) {
updateMappersOrder();
Expand All @@ -89,6 +91,7 @@ export function createMapperRegistry() {
mapper.worklet();
}
}
processingMappers = false;
}

function maybeRequestUpdates() {
Expand All @@ -101,7 +104,22 @@ export function createMapperRegistry() {
// if they want to make any assertions on the effects of animations being run.
mapperRun();
} else if (!runRequested) {
queueMicrotask(mapperRun);
if (processingMappers) {
// In general, we should avoid having mappers trigger updates as this may
// result in unpredictable behavior. Specifically, the updated value can
// be read by mappers that run later in the same frame but previous mappers
// would access the old value. Updating mappers during the mapper-run phase
// breaks the order in which we should execute the mappers. However, doing
// that is still a possibility and there are some instances where people use
// the API in that way, hence we need to prevent mapper-run phase falling into
// an infinite loop. We do that by detecting when mapper-run is requested while
// we are already in mapper-run phase, and in that case we use `requestAnimationFrame`
// instead of `queueMicrotask` which will schedule mapper run for the next
// frame instead of queuing another set of updates in the same frame.
requestAnimationFrame(mapperRun);
} else {
queueMicrotask(mapperRun);
}
runRequested = true;
}
}
Expand Down

0 comments on commit 7cdabc5

Please sign in to comment.