Skip to content

Re-send SharedValue config values from the UI thread after binding - #4538

Merged
m-bert merged 4 commits into
mainfrom
@mbert/shared-value-initial-sync
Oct 14, 2026
Merged

m-bert merged 4 commits into
mainfrom
@mbert/shared-value-initial-sync

Conversation

@m-bert

@m-bert m-bert commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

A gesture config value backed by a SharedValue could end up stale on the native side.

useGesture reads sharedValue.value on the JS thread when it prepares the full config and sends it asynchronously. The listener that forwards later writes is attached on the UI thread in a separate microtask. A write made on the UI thread between that read and the attach is never sent, and the full config lands with the old value. On re-renders the previous listener still forwards the write, but the full config and the partial update are issued from two threads and whichever lands last wins.

Changes in this PR:

  • bindSharedValues sends the SharedValue's current value through the listener body right after addListener, from the UI thread. That covers a write made before the listener existed and makes the UI-thread read the last write for the key. The listener body is a generic over the SharedValue type.
  • useGesture binds from the same flushed batch as setGestureHandlerConfig, right after it, so the re-send can't be dispatched before the full config on any code path.
  • The NativeProxy.updateGestureHandlerConfig comment no longer claims the update is flushed immediately.

Dropping SharedValue keys from the full config instead is not an option: setConfig resets absent keys to their defaults on every platform.

Test plan

Tested on the following code:
import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { GestureDetector, useTapGesture } from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
import { runOnUISync } from 'react-native-worklets';

// R1. `useGesture` reads `enabled.value` in its effect and queues the full
// config; the listener that forwards later writes is attached on the UI
// thread only in a later microtask. A UI-thread write in between is never
// sent, and the full config lands with the stale value.
//
// Steps: tap the box. Expected: nothing, `enabled` is false since mount.
// Bug: "tap activated, enabled.value = false".

export default function EmptyExample() {
  const enabled = useSharedValue(true);

  const tap = useTapGesture({
    enabled,
    onActivate: () => {
      console.log('tap activated, enabled.value =', enabled.value);
    },
  });

  // Runs right after useGesture's config effect: after it read `enabled.value`,
  // before the config listener exists on the UI thread. Synchronous so the
  // write lands in that window.
  useEffect(() => {
    runOnUISync(() => {
      'worklet';
      enabled.value = false;
    });
  }, [enabled]);

  return (
    <View style={styles.container}>
      <GestureDetector gesture={tap}>
        <View style={styles.box} />
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  box: {
    width: 160,
    height: 160,
    borderRadius: 16,
    backgroundColor: '#21a37c',
  },
});

Copilot AI lite review requested due to automatic review settings September 22, 2026 11:22
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1257369a-6ae9-43a3-afdd-a60fb34a2ac0
📥 Commits

Reviewing files that changed from the base of the PR and between 25316e6 and 4aba52d.

📒 Files selected for processing: 2
  • packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts
  • packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 4 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved gesture configuration synchronization when using shared values.
    • Applied the current shared-value state when a gesture handler is attached, so initial updates are not missed.
    • Queued shared-value bindings after native gesture configuration updates.
    • Skipped pending bindings if a gesture handler is cleaned up first.
  • Documentation
    • Clarified that native gesture configuration updates must be made directly and are applied asynchronously.

The gesture hook queues shared value binding after native configuration and skips binding if cleanup occurs first. Shared value listeners send their current values after registration. A native proxy comment describes direct worklet invocation and asynchronous native application.

Changes: Shared value binding

Layer / File(s) Summary
Configuration batching
packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts, packages/react-native-gesture-handler/src/v3/NativeProxy.ts
useGesture queues bindSharedValues after setting the native configuration and skips it if cleanup occurs first. The updateGestureHandlerConfig comment describes direct worklet invocation and asynchronous native application.
Listener initialization
packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts, packages/react-native-gesture-handler/src/__tests__/hitSlopSharedValue.test.ts
bindSharedValues registers a typed callback and sends the current shared value after registration. The test verifies the initial enabled: false update.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 4aba5

No actionable merge-blocking issue is established. Configuration precedes the shared-value update on the inspected native paths, and the test coverage gap alone does not indicate a product failure.

Pre-merge checks: Passed 3
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: re-sending SharedValue configuration values from the UI thread after binding to prevent stale native gesture configuration.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@m-bert
m-bert changed the base branch from @mbert/state-manager-android to main September 22, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The delayed binding can outlive effect cleanup, and the new resend behavior lacks regression coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

This PR prevents stale SharedValue values from overwriting newer UI-thread updates during v3 gesture configuration.

Changes:

  • Re-sends current values after binding listeners.
  • Orders binding after full native configuration.
  • Clarifies asynchronous native updates.
File Description
NativeProxy.ts Updates the native update comment.
reanimatedUtils.ts Adds typed listeners and initial value resend.
useGesture.ts Queues binding after full configuration.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts Outdated
@m-bert
m-bert requested a review from j-piasecki September 22, 2026 12:45
@m-bert
m-bert merged commit f5da9af into main Oct 14, 2026
11 checks passed
@m-bert
m-bert deleted the @mbert/shared-value-initial-sync branch October 14, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants