Skip to content

Commit

Permalink
Run listeners when calling sv.modify (#5306)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

This PR fixes `modify` method of shared value so that it runs all
listeners after the change even if the reference to the value does not
change.

Co-authored-by: @kmagiera 
Co-authored-by: @piaskowyk 

## Test plan

ModifyExample.tsx
  • Loading branch information
tomekzaw committed Oct 26, 2023
1 parent 55b5eb1 commit 9a42d8a
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 9 deletions.
54 changes: 54 additions & 0 deletions app/src/examples/ModifyExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, StyleSheet, View } from 'react-native';

import React from 'react';
import {
runOnUI,
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated';

export default function ModifyExample() {
const sv = useSharedValue([1]);

const handleModify = () => {
sv.modify((value) => {
'worklet';
value.push(value.length + 1);
return value;
});
};

const handleModifyWithoutArgument = () => {
sv.modify();
};

useDerivedValue(() => {
console.log('useDerivedValue', sv.value);
});

const handleRead = () => {
console.log('JS', sv.value);
runOnUI(() => {
console.log('UI', sv.value);
})();
};

return (
<View style={styles.container}>
<Button title="Modify" onPress={handleModify} />
<Button
title="Modify without argument"
onPress={handleModifyWithoutArgument}
/>
<Button title="Read" onPress={handleRead} />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
6 changes: 6 additions & 0 deletions app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import MeasureExample from './MeasureExample';
import Modal from './LayoutAnimations/Modal';
import ModalNewAPI from './LayoutAnimations/ModalNewAPI';
import ModalsExample from './SharedElementTransitions/Modals';
import ModifyExample from './ModifyExample';
import MountingUnmounting from './LayoutAnimations/MountingUnmounting';
import NativeModals from './LayoutAnimations/NativeModals';
import NestedNativeStacksWithLayout from './LayoutAnimations/NestedNativeStacksWithLayout';
Expand Down Expand Up @@ -133,6 +134,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Shareables',
screen: ShareablesExample,
},
ModifyExample: {
icon: '🪛',
title: 'Modify',
screen: ModifyExample,
},

// About

Expand Down
24 changes: 23 additions & 1 deletion docs/docs/core/useSharedValue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,29 @@ function App() {

sv.value.x = 50; // Reanimated loses reactivity 🚨

sv.value = { x: 50, y: 50 }; //
sv.value = { x: 50, y: 0 }; //
}
```

</Indent>

- When storing large arrays or complex objects in a shared value, you can use `.modify` method to alter the existing value instead of creating a new one.

<Indent>

```javascript
function App() {
const sv = useSharedValue([1, 2, 3]);

sv.value.push(1000); // Reanimated loses reactivity 🚨

sv.value = [...sv.value, 1000]; // works, but creates a new copy ⚠️

sv.modify((value) => {
'worklet';
value.push(1000); //
return value;
});
}
```

Expand Down
2 changes: 1 addition & 1 deletion src/reanimated2/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface SharedValue<Value> {
value: Value;
addListener: (listenerID: number, listener: (value: any) => void) => void;
removeListener: (listenerID: number) => void;
modify: (modifier: (value: any) => any) => void;
modify: (modifier?: (value: any) => any) => void;
}

// The below type is used for HostObjects returned by the JSI API that don't have
Expand Down
19 changes: 15 additions & 4 deletions src/reanimated2/mutables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export function makeUIMutable<T>(
get _value(): T {
return value;
},
modify: (modifier?: (value: T) => T) => {
valueSetter(self, modifier !== undefined ? modifier(value) : value, true);
},
addListener: (id: number, listener: (newValue: T) => void) => {
listeners.set(id, listener);
},
Expand Down Expand Up @@ -117,10 +120,18 @@ export function makeMutable<T>(
}
return value;
},
modify: (modifier: (value: T) => T) => {
runOnUI(() => {
mutable.value = modifier(mutable.value);
})();
modify: (modifier?: (value: T) => T) => {
if (!SHOULD_BE_USE_WEB) {
runOnUI(() => {
mutable.modify(modifier);
})();
} else {
valueSetter(
mutable,
modifier !== undefined ? modifier(mutable.value) : mutable.value,
true
);
}
},
addListener: (id: number, listener: (value: T) => void) => {
if (!SHOULD_BE_USE_WEB) {
Expand Down
10 changes: 7 additions & 3 deletions src/reanimated2/valueSetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { AnimationObject, AnimatableValue } from './commonTypes';
import type { Descriptor } from './hook/commonTypes';

export function valueSetter(sv: any, value: any): void {
export function valueSetter(sv: any, value: any, forceUpdate = false): void {
'worklet';
const previousAnimation = sv._animation;
if (previousAnimation) {
Expand All @@ -23,7 +23,11 @@ export function valueSetter(sv: any, value: any): void {
// and triggering the mappers that treat this value as an input
// this happens when the animation's target value(stored in animation.current until animation.onStart is called) is set to the same value as a current one(this._value)
// built in animations that are not higher order(withTiming, withSpring) hold target value in .current
if (sv._value === animation.current && !animation.isHigherOrder) {
if (
sv._value === animation.current &&
!animation.isHigherOrder &&
!forceUpdate
) {
animation.callback && animation.callback(true);
return;
}
Expand Down Expand Up @@ -66,7 +70,7 @@ export function valueSetter(sv: any, value: any): void {
} else {
// prevent setting again to the same value
// and triggering the mappers that treat this value as an input
if (sv._value === value) {
if (sv._value === value && !forceUpdate) {
return;
}
sv._value = value as Descriptor | AnimatableValue;
Expand Down

0 comments on commit 9a42d8a

Please sign in to comment.