Skip to content

Conversation

akwasniewski
Copy link
Contributor

@akwasniewski akwasniewski commented Aug 12, 2025

Description

This PR changes how config is handled on web, following #3658 it separates update and set functionality, which allows passing only relevant, changed fields to update, as opposed to the entire config. Moreover it removes the config field from GestureHandler - adds necessary fields as class fields, this mimics how config is handled on iOS / Android and simplifies the code (previously some fields were both both config and class fields).

Test plan

The update functionality was tested on the following code.

import * as React from 'react';
import { Animated, Button } from 'react-native';
import { useSharedValue } from 'react-native-reanimated';
import {
  GestureHandlerRootView,
  NativeDetector,
  useGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const [visible, setVisible] = React.useState(true);
  const taps = useSharedValue(2);

  const gesture = useGesture('TapGestureHandler', {
    onEnd: () => {
      console.log('Tap detected. Required number of taps:', taps.value);
      taps.set(taps.value + 1);
    },
    numberOfTaps: taps,
  });

  return (
    <GestureHandlerRootView
      style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
      <Button
        title="Toggle visibility"
        onPress={() => {
          setVisible(!visible);
        }}
      />

      {visible && (
        <NativeDetector gesture={gesture}>
          <Animated.View
            style={[
              {
                width: 150,
                height: 150,
                backgroundColor: 'blue',
                opacity: 0.5,
                borderWidth: 10,
                borderColor: 'green',
                marginTop: 20,
                marginLeft: 40,
              },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}

@akwasniewski akwasniewski requested a review from m-bert August 12, 2025 11:49
@akwasniewski akwasniewski marked this pull request as ready for review August 14, 2025 08:56
Copy link
Contributor

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

Given that in updateGestureConfig we have:

this._config = {
  enabled,
  dispatchesAnimatedEvents,
  ...props,
};

updateGestureConfig acts as setGestureConfig. The point of separating this method in module was to have set method which sets the config to new one and update method which updates only changed fields.

Right now doing this:

console.log(this.config);
this._config = {
  enabled,
  dispatchesAnimatedEvents,
  ...props,
};
console.log(this.config);

yields this:

Image

@akwasniewski
Copy link
Contributor Author

The point of separating this method in module was to have set method which sets the config to new one and update method which updates only changed fields.

Okay, I had previously not understood this was the point, as your PR didn't add this, it simply renamed the function. Even before your PR checking if the fields exist was already built into setConfig. I added it in 41f615ef.

@akwasniewski akwasniewski requested a review from m-bert August 18, 2025 11:15
@m-bert
Copy link
Contributor

m-bert commented Aug 18, 2025

Okay, I had previously not understood this was the point, as your PR didn't add this, it simply renamed the function.

Not really. It added setGestureHandlerConfig into module spec and updated behavior of updateGestureHandlerConfig such that it really updates the config, not resets it 😉

@akwasniewski
Copy link
Contributor Author

Not really. It added setGestureHandlerConfig into module spec and updated behavior of updateGestureHandlerConfig such that it really updates the config, not resets it 😉

Yeah, I could have worded that better. I meant that iOS / android implementations required fewer changes within the GestureHandler, I did not notice that web required more.

Comment on lines 640 to 643
if (
config.shouldCancelWhenOutside !== undefined &&
this.config.shouldCancelWhenOutside !== undefined
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (
config.shouldCancelWhenOutside !== undefined &&
this.config.shouldCancelWhenOutside !== undefined
) {
if (config.shouldCancelWhenOutside !== undefined) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Also this check is quite weird - if config.shouldCancelWhenOutside is defined, then it will be assigned into this.config.shouldCancelWhenOutside in the loop above. And if config.shouldCancelWhenOutside is undefined, then nothing really happens.

Note, I assumed that the for loop above doesn't check for key differences.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, got carried away 4fd97f8a1b8

Comment on lines 647 to 650
if (
config.dispatchesAnimatedEvent !== undefined &&
this.config.dispatchesAnimatedEvents !== undefined
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (
config.dispatchesAnimatedEvent !== undefined &&
this.config.dispatchesAnimatedEvents !== undefined
) {
if (config.dispatchesAnimatedEvent !== undefined) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checking undefined was just to make eslint happy, fixed in 4fd97f8a1b8

Comment on lines 634 to 638
for (const key of Object.keys(config)) {
if (this.config[key] !== config[key]) {
this.config[key] = config[key];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for (const key of Object.keys(config)) {
if (this.config[key] !== config[key]) {
this.config[key] = config[key];
}
}
for (const key of Object.keys(config)) {
this.config[key] = config[key];
}

I don't think we need this check, I'd just assign new values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, fixed 4fd97f8a1b8

@akwasniewski akwasniewski marked this pull request as draft August 21, 2025 08:57
@akwasniewski akwasniewski changed the title [Web] Bind SharedValues in handler config [Web] Config refactor - update logic and drop class field Aug 21, 2025
@akwasniewski akwasniewski marked this pull request as ready for review August 21, 2025 09:36
@akwasniewski akwasniewski requested a review from m-bert August 21, 2025 09:36
Base automatically changed from @mbert/shared-values to next August 22, 2025 12:11
Copy link
Member

@j-piasecki j-piasecki left a comment

Choose a reason for hiding this comment

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

Took a quick look, and the PR generally looks good. Can you please merge the next branch so that only actually modified files are shown in the diff?

}

if (config.dispatchesAnimatedEvents !== undefined) {
this.forAnimated = config.dispatchesAnimatedEvents;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
this.forAnimated = config.dispatchesAnimatedEvents;
this.dispatchesAnimatedEvents = config.dispatchesAnimatedEvents;

Can we align those names?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 2da4640

Comment on lines 84 to 104
if (config.activeOffsetXStart !== undefined) {
this.activeOffsetXStart = config.activeOffsetXStart;
this.hasCustomActivationCriteria = true;
}

if (this.config.activeOffsetXStart !== undefined) {
this.activeOffsetXStart = this.config.activeOffsetXStart;

if (this.config.activeOffsetXEnd === undefined) {
this.activeOffsetXEnd = Number.MAX_SAFE_INTEGER;
}
if (config.activeOffsetXEnd !== undefined) {
this.activeOffsetXEnd = config.activeOffsetXEnd;
this.hasCustomActivationCriteria = true;
}

if (this.config.activeOffsetXEnd !== undefined) {
this.activeOffsetXEnd = this.config.activeOffsetXEnd;

if (this.config.activeOffsetXStart === undefined) {
this.activeOffsetXStart = Number.MIN_SAFE_INTEGER;
}
if (
this.activeOffsetXStart !== undefined &&
this.activeOffsetXEnd === undefined
) {
this.activeOffsetXEnd = Number.MAX_SAFE_INTEGER;
} else if (
this.activeOffsetXStart === undefined &&
this.activeOffsetXEnd !== undefined
) {
this.activeOffsetXStart = Number.MIN_SAFE_INTEGER;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is the logic with Number.MAX_SAFE_INTEGER needed? Wouldn't that be handled by resetConfig?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, it is handled by resetConfig. Thanks, removed in f490152.

Comment on lines 106 to 126
if (config.failOffsetXStart !== undefined) {
this.failOffsetXStart = config.failOffsetXStart;
this.hasCustomActivationCriteria = true;
}

if (this.config.failOffsetXEnd !== undefined) {
this.failOffsetXEnd = this.config.failOffsetXEnd;

if (this.config.failOffsetXStart === undefined) {
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
}
if (config.failOffsetXEnd !== undefined) {
this.failOffsetXEnd = config.failOffsetXEnd;
this.hasCustomActivationCriteria = true;
}

if (this.config.activeOffsetYStart !== undefined) {
this.activeOffsetYStart = this.config.activeOffsetYStart;

if (this.config.activeOffsetYEnd === undefined) {
this.activeOffsetYEnd = Number.MAX_SAFE_INTEGER;
}
if (
this.failOffsetXStart !== undefined &&
this.failOffsetXEnd === undefined
) {
this.failOffsetXEnd = Number.MAX_SAFE_INTEGER;
} else if (
this.failOffsetXStart === undefined &&
this.failOffsetXEnd !== undefined
) {
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
}
Copy link
Member

Choose a reason for hiding this comment

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

Same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in f490152.

Comment on lines 128 to 148
if (config.activeOffsetYStart !== undefined) {
this.activeOffsetYStart = config.activeOffsetYStart;
this.hasCustomActivationCriteria = true;
}

if (this.config.activeOffsetYStart === undefined) {
this.activeOffsetYStart = Number.MIN_SAFE_INTEGER;
}
if (config.activeOffsetYEnd !== undefined) {
this.activeOffsetYEnd = config.activeOffsetYEnd;
this.hasCustomActivationCriteria = true;
}

if (this.config.failOffsetYStart !== undefined) {
this.failOffsetYStart = this.config.failOffsetYStart;
if (
this.activeOffsetYStart !== undefined &&
this.activeOffsetYEnd === undefined
) {
this.activeOffsetYEnd = Number.MAX_SAFE_INTEGER;
} else if (
this.activeOffsetYStart === undefined &&
this.activeOffsetYEnd !== undefined
) {
this.activeOffsetYStart = Number.MIN_SAFE_INTEGER;
}
Copy link
Member

Choose a reason for hiding this comment

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

Same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in f490152.

Comment on lines 150 to 170
if (config.failOffsetYStart !== undefined) {
this.failOffsetYStart = config.failOffsetYStart;
this.hasCustomActivationCriteria = true;
}

if (this.config.failOffsetYEnd !== undefined) {
this.failOffsetYEnd = this.config.failOffsetYEnd;
if (config.failOffsetYEnd !== undefined) {
this.failOffsetYEnd = config.failOffsetYEnd;
this.hasCustomActivationCriteria = true;
}

if (this.config.failOffsetYStart === undefined) {
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
}
if (
this.failOffsetYStart !== undefined &&
this.failOffsetYEnd === undefined
) {
this.failOffsetYEnd = Number.MAX_SAFE_INTEGER;
} else if (
this.failOffsetYStart === undefined &&
this.failOffsetYEnd !== undefined
) {
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
}
Copy link
Member

Choose a reason for hiding this comment

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

Same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in f490152.

@@ -156,7 +153,7 @@ export class GestureHandlerWebDelegate
}

private setUserSelect(isHandlerEnabled: boolean) {
Copy link
Contributor

@m-bert m-bert Aug 26, 2025

Choose a reason for hiding this comment

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

It is probably out of scope of this PR, but if we have access to this.gestureHandler we could do this.gestureHandler.enabled instead of passing it as argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think If the delegate is already being refactored we might as well add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 847f3ed

Copy link
Contributor

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

LGTM

public get enableContextMenu() {
return this._enableContextMenu;
}
protected set enableContextMenu(value) {
Copy link
Contributor

@m-bert m-bert Aug 26, 2025

Choose a reason for hiding this comment

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

Should they be protected? If they're not used outside of this class we could assign directly to backing field and remove setters.

However this approach is also ok, so feel free to choose the one you prefer.

Copy link
Contributor Author

@akwasniewski akwasniewski Aug 27, 2025

Choose a reason for hiding this comment

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

Sure, I removed setters that are not used outside of the class, there is no reason to bloat the code e87ea11

@akwasniewski akwasniewski merged commit b18935a into next Aug 27, 2025
1 check passed
@akwasniewski akwasniewski deleted the @akwasniewski/shared-values-web branch August 27, 2025 08:13
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