Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
737e2c2
Add repro
tomekzaw Nov 4, 2025
ea46171
Add `PropsRegistryGarbageCollector`
tomekzaw Nov 4, 2025
6fe267a
Implement `getSettledUpdates`
tomekzaw Nov 4, 2025
faef685
Implement `removeEntriesOlderThanTimestamp`
tomekzaw Nov 4, 2025
2c2e566
Change thresholds
tomekzaw Nov 4, 2025
736b372
Move JSI logic to `getTagsOlderThanTimestamp`
tomekzaw Nov 4, 2025
8ef1ab1
Add locks
tomekzaw Nov 4, 2025
533cc28
Add TODO
tomekzaw Nov 4, 2025
1a5db8d
Add argument
tomekzaw Nov 4, 2025
09a3ec7
Adjust threshold
tomekzaw Nov 4, 2025
286bdbc
Fix double lock
tomekzaw Nov 4, 2025
38f7a29
Fix formatting
tomekzaw Nov 4, 2025
921ce60
Reformat files
tomekzaw Nov 4, 2025
db291cc
Rename methods
tomekzaw Nov 4, 2025
93f537f
Get rid of `collectProps` call
tomekzaw Nov 4, 2025
7d01b81
Get rid of old view tags set
tomekzaw Nov 4, 2025
97db95f
Add TODO
tomekzaw Nov 4, 2025
b488787
Change threshold
tomekzaw Nov 5, 2025
ad7acfe
Add TODOs
tomekzaw Nov 5, 2025
ae7b420
Add example
tomekzaw Nov 5, 2025
dad2b18
Reformat files
tomekzaw Nov 5, 2025
e0c057c
Remove lime
tomekzaw Nov 5, 2025
662e145
Add TODO
tomekzaw Nov 5, 2025
361929a
Add comment
tomekzaw Nov 5, 2025
629cba8
Add TODO
tomekzaw Nov 5, 2025
76bb9b5
Add counter
tomekzaw Nov 5, 2025
eff0ce5
Bump react-native-gesture-handler
tomekzaw Nov 5, 2025
54acb13
Add `unprocessColorsInProps`
tomekzaw Nov 5, 2025
7e24996
Unprocess `boxShadow`
tomekzaw Nov 5, 2025
e68ee08
Remove logs
tomekzaw Nov 5, 2025
1177097
Add `@ts-ignore`
tomekzaw Nov 5, 2025
e0feb00
Merge branch 'main' into @tomekzaw/sync-state
tomekzaw Nov 5, 2025
4420780
Those are rookie numbers
tomekzaw Nov 5, 2025
bf2347e
Add TODOs
tomekzaw Nov 6, 2025
9e77aee
Merge branch 'main' into @tomekzaw/sync-state
tomekzaw Nov 6, 2025
11444c9
Add support for `DynamicColorIOS`
tomekzaw Nov 6, 2025
e6c5866
Add instructions
tomekzaw Nov 6, 2025
ce9db98
Merge branch 'main' into @tomekzaw/sync-state
tomekzaw Nov 7, 2025
585a0fb
Merge branch 'main' into @tomekzaw/sync-state
tomekzaw Nov 7, 2025
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
2 changes: 1 addition & 1 deletion apps/common-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"fuse.js": "patch:fuse.js@npm%3A7.1.0#~/.yarn/patches/fuse.js-npm-7.1.0-5dcae892a6.patch",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-native-gesture-handler": "2.28.0",
"react-native-gesture-handler": "2.29.1",
"react-native-pager-view": "7.0.0",
"react-native-reanimated": "workspace:*",
"react-native-safe-area-context": "5.6.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import React, { useCallback } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import type {
GestureUpdateEvent,
PanGestureChangeEventPayload,
Expand Down Expand Up @@ -66,12 +66,20 @@ export default function ScreenStackHeaderConfigBackgroundColorExample() {
};
}, [offset]);

const [counter, setCounter] = React.useState(0);

const handleIncrement = useCallback(() => {
setCounter((prev) => prev + 1);
}, []);

return (
<GestureHandlerRootView style={styles.root}>
<ScreenStack style={styles.container}>
<Screen>
<AnimatedScreenStackHeaderConfig animatedProps={animatedProps} />
<View style={styles.container}>
<Text>Counter: {counter}</Text>
<Button title="Increase counter" onPress={handleIncrement} />
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.ball, animatedStyles]} />
</GestureDetector>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';

const instructions = [
'1. Press "Animate width and color" button',
'2. Wait until the animated styles are synced back to React (about 3 seconds)',
'3. Press "Increase counter" button',
'4. The view width and color not change',
].join('\n');

interface ButtonProps {
title: string;
onPress: () => void;
}

function Button({ title, onPress }: ButtonProps) {
// We use a custom button component because the one from React Native
// triggers additional renders when pressed.

return (
<View onTouchEnd={onPress} style={styles.buttonView}>
<Text style={styles.buttonText}>{title}</Text>
</View>
);
}

export default function SyncBackToReactExample() {
const [count, setCount] = React.useState(0);

const sv = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => {
return {
width: 20 + sv.value * 200,
backgroundColor: interpolateColor(sv.value, [0, 1], ['red', 'lime']),
};
});

const handleAnimateWidth = useCallback(() => {
sv.value = withSpring(Math.random());
}, [sv]);

const handleIncreaseCounter = useCallback(() => {
setCount((c) => c + 1);
}, []);

return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Animated.View style={[styles.box, animatedStyle]} />
<Animated.View style={[styles.box, animatedStyle]} />
<Animated.View style={[styles.box, animatedStyle]} />
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Animate width and color" onPress={handleAnimateWidth} />
<Text>Counter: {count}</Text>
<Button title="Increase counter" onPress={handleIncreaseCounter} />
<Text style={styles.instructions}>{instructions}</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
height: 20,
backgroundColor: 'navy',
},
buttonView: {
margin: 20,
},
buttonText: {
fontSize: 20,
color: 'dodgerblue',
},
instructions: {
marginHorizontal: 20,
},
});
6 changes: 6 additions & 0 deletions apps/common-app/src/apps/reanimated/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import ReducedMotionLayoutExample from './LayoutAnimations/ReducedMotionLayoutEx
import ReparentingExample from './LayoutAnimations/ReparentingExample';
import SpringLayoutAnimation from './LayoutAnimations/SpringLayoutAnimation';
import SwipeableList from './LayoutAnimations/SwipeableList';
import SyncBackToReactExample from './SyncBackToReactExample';
import ViewFlatteningExample from './LayoutAnimations/ViewFlattening';
import ViewRecyclingExample from './LayoutAnimations/ViewRecyclingExample';
import LettersExample from './LettersExample';
Expand Down Expand Up @@ -172,6 +173,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'FPS',
screen: FpsExample,
},
SyncBackToReactExample: {
icon: '🔄',
title: 'Sync back to React',
screen: SyncBackToReactExample,
},
ScrollPerformanceExample: {
icon: '🚁',
title: 'Scroll performance',
Expand Down
6 changes: 3 additions & 3 deletions apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2520,7 +2520,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNGestureHandler (2.28.0):
- RNGestureHandler (2.29.1):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3186,8 +3186,8 @@ SPEC CHECKSUMS:
RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82
RNCClipboard: 4b58c780f63676367640f23c8e114e9bd0cf86ac
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
RNGestureHandler: f1dd7f92a0faa2868a919ab53bb9d66eb4ebfcf5
RNReanimated: 97ebf4d3c76929b6b0f866cfbd41c49b3a0d2dbf
RNGestureHandler: e1cf8ef3f11045536eed6bd4f132b003ef5f9a5f
RNReanimated: 93f0559693461402ff0450e86160b2200eae681a
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
RNSVG: 8c0bbfa480a24b24468f1c76bd852a4aac3178e6
RNWorklets: f54a415f73a3fc653bfe65e599872fdc6bca0477
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ RootShadowNode::Unshared ReanimatedCommitHook::shadowTreeWillCommit(
auto lock = updatesRegistryManager_->lock();

PropsMap propsMap = updatesRegistryManager_->collectProps();
LOG(INFO) << "ReanimatedCommitHook propsMap size=" << propsMap.size();
updatesRegistryManager_->cancelCommitAfterPause();

rootNode = cloneShadowTreeWithNewProps(*rootNode, propsMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ static inline std::shared_ptr<const ShadowNode> shadowNodeFromValue(
}
#endif

void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operations) {
void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operations, const double timestamp) {
auto operationsArray = operations.asObject(rt).asArray(rt);

for (size_t i = 0, length = operationsArray.size(rt); i < length; ++i) {
Expand All @@ -23,11 +23,65 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation

const jsi::Value &updates = item.getProperty(rt, "updates");
addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates));

timestampMap_[shadowNode->getTag()] = timestamp;
}
}

void AnimatedPropsRegistry::remove(const Tag tag) {
updatesRegistry_.erase(tag);
}

jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp(jsi::Runtime &rt, const double timestamp) {
std::unordered_map<Tag, folly::dynamic> updatesMap;
{
auto lock1 = lock();

for (const auto &[tag, pair] : updatesRegistry_) {
const auto &[shadowNode, props] = pair;
const auto viewTag = shadowNode->getTag();

const auto viewTimestamp = timestampMap_.at(viewTag);
if (viewTimestamp >= timestamp) {
continue;
}

auto it = updatesMap.find(viewTag);
if (it == updatesMap.cend()) {
folly::dynamic styleProps = folly::dynamic::object();
styleProps.update(props);
updatesMap[viewTag] = styleProps;
} else {
it->second.update(props);
}
}
}

const jsi::Array array(rt, updatesMap.size());
size_t idx = 0;
for (const auto &[viewTag, styleProps] : updatesMap) {
const jsi::Object item(rt);
item.setProperty(rt, "viewTag", viewTag);
item.setProperty(rt, "styleProps", jsi::valueFromDynamic(rt, styleProps));
array.setValueAtIndex(rt, idx++, item);
}

return jsi::Value(rt, array);
}

void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timestamp) {
auto lock1 = lock();

for (auto it = timestampMap_.begin(); it != timestampMap_.end();) {
const auto viewTag = it->first;
const auto viewTimestamp = it->second;
if (viewTimestamp < timestamp) {
it = timestampMap_.erase(it);
updatesRegistry_.erase(viewTag);
} else {
it++;
}
}
}

} // namespace reanimated
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
#include <react/renderer/uimanager/UIManager.h>

#include <memory>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

namespace reanimated {

class AnimatedPropsRegistry : public UpdatesRegistry {
public:
void update(jsi::Runtime &rt, const jsi::Value &operations);
void update(jsi::Runtime &rt, const jsi::Value &operations, const double timestamp);
void remove(Tag tag) override;
jsi::Value getUpdatesOlderThanTimestamp(jsi::Runtime &rt, const double timestamp);
void removeUpdatesOlderThanTimestamp(const double timestamp);

private:
std::unordered_map<Tag, double> timestampMap_; // viewTag -> timestamp, protected by `mutex_`
};

} // namespace reanimated
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <reanimated/NativeModules/PropValueProcessor.h>
#include <reanimated/NativeModules/ReanimatedModuleProxy.h>
Expand Down Expand Up @@ -92,7 +93,8 @@ void ReanimatedModuleProxy::init(const PlatformDepMethodsHolder &platformDepMeth
return;
}

strongThis->animatedPropsRegistry_->update(rt, operations);
const auto timestamp = strongThis->getAnimationTimestamp_();
strongThis->animatedPropsRegistry_->update(rt, operations, timestamp);
};

auto measure = [weakThis = weak_from_this()](jsi::Runtime &rt, const jsi::Value &shadowNodeValue) -> jsi::Value {
Expand Down Expand Up @@ -453,6 +455,20 @@ void ReanimatedModuleProxy::unregisterCSSTransition(jsi::Runtime &rt, const jsi:
cssTransitionsRegistry_->remove(viewTag.asNumber());
}

jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) {
// TODO: use unified timestamp
const auto currentTimestamp = getAnimationTimestamp_();

// TODO: flush updates from CSS animations and CSS transitions registries

// TODO: move removing old updates to separate method?
// TODO: fix bug when threshold difference is smaller than 1 second
// TODO: find a better way to obtain timestamp for removing updates
animatedPropsRegistry_->removeUpdatesOlderThanTimestamp(currentTimestamp - 2000); // 2 seconds

return animatedPropsRegistry_->getUpdatesOlderThanTimestamp(rt, currentTimestamp - 1000); // 1 second
}

bool ReanimatedModuleProxy::handleEvent(
const std::string &eventName,
const int emitterReactTag,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ class ReanimatedModuleProxy : public ReanimatedModuleProxySpec,
void updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &configUpdates) override;
void unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) override;

jsi::Value getSettledUpdates(jsi::Runtime &rt) override;

void cssLoopCallback(const double /*timestampMs*/);

void dispatchCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ static jsi::Value REANIMATED_SPEC_PREFIX(
return jsi::Value::undefined();
}

static jsi::Value REANIMATED_SPEC_PREFIX(
getSettledUpdates)(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value *args, size_t) {
return static_cast<ReanimatedModuleProxySpec *>(&turboModule)->getSettledUpdates(rt);
}

ReanimatedModuleProxySpec::ReanimatedModuleProxySpec(const std::shared_ptr<CallInvoker> &jsInvoker)
: TurboModule("NativeReanimated", jsInvoker) {
methodMap_["registerEventHandler"] = MethodMetadata{3, REANIMATED_SPEC_PREFIX(registerEventHandler)};
Expand Down Expand Up @@ -171,6 +176,8 @@ ReanimatedModuleProxySpec::ReanimatedModuleProxySpec(const std::shared_ptr<CallI
methodMap_["registerCSSTransition"] = MethodMetadata{2, REANIMATED_SPEC_PREFIX(registerCSSTransition)};
methodMap_["updateCSSTransition"] = MethodMetadata{2, REANIMATED_SPEC_PREFIX(updateCSSTransition)};
methodMap_["unregisterCSSTransition"] = MethodMetadata{1, REANIMATED_SPEC_PREFIX(unregisterCSSTransition)};

methodMap_["getSettledUpdates"] = MethodMetadata{1, REANIMATED_SPEC_PREFIX(getSettledUpdates)};
}

} // namespace reanimated
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class JSI_EXPORT ReanimatedModuleProxySpec : public TurboModule {
registerCSSTransition(jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &transitionConfig) = 0;
virtual void updateCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &configUpdates) = 0;
virtual void unregisterCSSTransition(jsi::Runtime &rt, const jsi::Value &viewTag) = 0;

virtual jsi::Value getSettledUpdates(jsi::Runtime &rt) = 0;
};

} // namespace reanimated
Loading
Loading