Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 149 additions & 0 deletions app/src/examples/ShareableFreezingExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
Button,
Text,
StyleSheet,
View,
TurboModuleRegistry,
} from 'react-native';

import React from 'react';
import { makeShareableCloneRecursive } from 'react-native-reanimated';

export default function FreezingShareables() {
return (
<View style={styles.container}>
<View style={styles.textAndButton}>
<Text style={styles.text}>⚠️</Text>
<Button
title="Modify converted array"
onPress={tryModifyConvertedArray}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>🤫</Text>
<Button
title="Modify converted remote function"
onPress={tryModifyConvertedRemoteFunction}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>⚠️</Text>
<Button
title="Modify converted host object"
onPress={tryModifyConvertedHostObject}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>⚠️</Text>
<Button
title="Modify converted plain object"
onPress={tryModifyConvertedPlainObject}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>🤫</Text>
<Button
title="Modify converted RegExp literal"
onPress={tryModifyConvertedRegExpLiteral}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>🤫</Text>
<Button
title="Modify converted RegExp instance"
onPress={tryModifyConvertedRegExpInstance}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>🤫</Text>
<Button
title="Modify converted ArrayBuffer"
onPress={tryModifyConvertedArrayBuffer}
/>
</View>
<View style={styles.textAndButton}>
<Text style={styles.text}>🤫</Text>
<Button
title="Modify converted Int32Array"
onPress={tryModifyConvertedInt32Array}
/>
</View>
</View>
);
}

function tryModifyConvertedArray() {
const obj = [1, 2, 3];
makeShareableCloneRecursive(obj);
obj[0] = 2; // should warn beacuse it's frozen
}

function tryModifyConvertedRemoteFunction() {
const obj = () => {};
obj.prop = 1;
makeShareableCloneRecursive(obj);
obj.prop = 2; // should warn because it's frozen
}

function tryModifyConvertedHostObject() {
const obj = TurboModuleRegistry.get('Clipboard');
if (!obj) {
console.warn('No host object found.');
return;
}
makeShareableCloneRecursive(obj);
// @ts-expect-error
obj.prop = 2; // shouldn't warn because it's not frozen
}

function tryModifyConvertedPlainObject() {
const obj = {
prop: 1,
};
makeShareableCloneRecursive(obj);
obj.prop = 2; // should warn because it's frozen
}

function tryModifyConvertedRegExpLiteral() {
const obj = /a/;
makeShareableCloneRecursive(obj);
// @ts-expect-error
obj.prop = 2; // shouldn't warn because it's not frozen
}

function tryModifyConvertedRegExpInstance() {
const obj = new RegExp('a');
makeShareableCloneRecursive(obj);
// @ts-expect-error
obj.prop = 2; // shouldn't warn because it's not frozen
}

function tryModifyConvertedArrayBuffer() {
const obj = new ArrayBuffer(8);
makeShareableCloneRecursive(obj);
// @ts-expect-error
obj.prop = 2; // shouldn't warn because it's not frozen
}

function tryModifyConvertedInt32Array() {
const obj = new Int32Array(2);
makeShareableCloneRecursive(obj);
obj[1] = 2; // shouldn't warn because it's not frozen
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
textAndButton: {
width: '90%',
flexDirection: 'row',
alignItems: 'center',
},
text: {
fontSize: 32,
marginRight: 10,
},
});
6 changes: 6 additions & 0 deletions app/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import HabitsExample from './LayoutAnimations/HabitsExample';
import MemoExample from './MemoExample';
import PerformanceMonitorExample from './PerfomanceMonitorExample';
import ScreenTransitionExample from './ScreenTransitionExample';
import FreezingShareablesExample from './ShareableFreezingExample';

interface Example {
icon?: string;
Expand Down Expand Up @@ -169,6 +170,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Memo',
screen: MemoExample,
},
FreezingShareablesExample: {
icon: '🥶',
title: 'Freezing shareables',
screen: FreezingShareablesExample,
},

// About

Expand Down
80 changes: 80 additions & 0 deletions docs/docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,86 @@ LogBox.ignoreLogs([

See [the accessibility overview](accessibility) to learn more about Reduced Motion.

### Tried to modify key of an object which has been converted to a shareable.

**Problem:** This warning is displayed to inform the user that a shared value should be used or an object used in a worklet should be accessed more granularly.

#### 1. Not using shared values.

You might get this warning when you do something along the lines of:

```js
const obj = { prop: 1 };

function worklet() {
'worklet';
console.log(obj.prop);
}

runOnUI(worklet)();
obj.prop = 2; // Warning: Tried to modify key `prop` of an object which has been already passed to a worklet.
runOnUI(worklet)();
```

and expect the results to be `1` and `2`. However, the results will be `1` and `1` because `obj` is not a shared value and is only copied to UI runtime once. Therefore, in development builds, we make the object immutable and add this warning after copying it to signal that it's not a valid use of Reanimated. To fix this, you should use a shared value instead:

**Solution:**

```diff
-const obj = { prop: 1 };
+const sv = useSharedValue({ prop: 1 });

function worklet() {
'worklet';
- console.log(obj.prop);
+ console.log(sv.value.prop);
}

runOnUI(worklet)();
-obj.prop = 2; // Warning: Tried to modify key `prop` of an object which has been already passed to a worklet.
+sv.value = { prop: 2 }; // Everything is fine here.
+// Keep in mind that you cannot modify the property directly with `sv.value.prop = 2` unless you use the `modify` method.
runOnUI(worklet)();
```

#### 2. Not accessing object properties granularly.

When you access an object property in a worklet, you might do something like this:

```js
const obj = { propAccessedInWorklet: 1, propNotAccessedInWorklet: 2 };

function worklet() {
'worklet';
console.log(obj.propAccessedInWorklet);
}

runOnUI(worklet)();
obj.propNotAccessedInWorklet = 3; // Warning: Tried to modify key `prop` of an object which has been already passed to a worklet.
```

The warning is displayed due to the mechanism explained in the previous case. Since we copy the whole object `obj` instead its accessed properties, it's immutable.

**Solution:**

Assign accessed properties to variables beforehand and use those in the worklet:

```diff
const obj = { propAccessedInWorklet: 1, propNotAccessedInWorklet: 2 };

+const propAccessedInWorklet = obj.propAccessedInWorklet;
+
function worklet() {
'worklet';
- console.log(obj.propAccessedInWorklet);
+ console.log(propAccessedInWorklet);
}

runOnUI(worklet)();
-obj.propNotAccessedInWorklet = 3; // Warning: Tried to modify key `prop` of an object which has been already passed to a worklet.
+obj.propNotAccessedInWorklet = 3; // Everything is fine here.
```

## Threading issues

### Tried to synchronously call a non-worklet function on the UI thread
Expand Down
54 changes: 41 additions & 13 deletions src/reanimated2/shareables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ export function makeShareableCloneRecursive<T>(
toAdapt = value.map((element) =>
makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1)
);
freezeObjectIfDev(value);
} else if (isTypeFunction && !isWorkletFunction(value)) {
// this is a remote function
toAdapt = value;
freezeObjectIfDev(value);
} else if (isHostObject(value)) {
// for host objects we pass the reference to the object as shareable and
// then recreate new host object wrapping the same instance on the UI thread.
Expand Down Expand Up @@ -191,6 +193,7 @@ Offending code was: \`${getWorkletCode(value)}\``);
depth + 1
);
}
freezeObjectIfDev(value);
} else if (value instanceof RegExp) {
const pattern = value.source;
const flags = value.flags;
Expand Down Expand Up @@ -255,23 +258,14 @@ Offending code was: \`${getWorkletCode(value)}\``);
shareableMappingCache.set(value, inaccessibleObject);
return inaccessibleObject;
}
if (__DEV__) {
// we freeze objects that are transformed to shareable. This should help
// detect issues when someone modifies data after it's been converted to
// shareable. Meaning that they may be doing a faulty assumption in their
// code expecting that the updates are going to automatically populate to
// the object sent to the UI thread. If the user really wants some objects
// to be mutable they should use shared values instead.
Object.freeze(value);
}
const adopted = NativeReanimatedModule.makeShareableClone(
const adapted = NativeReanimatedModule.makeShareableClone(
toAdapt,
shouldPersistRemote,
value
);
shareableMappingCache.set(value, adopted);
shareableMappingCache.set(adopted);
return adopted;
shareableMappingCache.set(value, adapted);
shareableMappingCache.set(adapted);
return adapted;
}
}
return NativeReanimatedModule.makeShareableClone(
Expand Down Expand Up @@ -306,6 +300,40 @@ function isRemoteFunction<T>(value: {
return !!value.__remoteFunction;
}

/**
* We freeze
* - arrays,
* - remote functions,
* - plain JS objects,
*
* that are transformed to a shareable with a meaningful warning.
* This should help detect issues when someone modifies data after it's been converted.
* Meaning that they may be doing a faulty assumption in their
* code expecting that the updates are going to automatically propagate to
* the object sent to the UI thread. If the user really wants some objects
* to be mutable they should use shared values instead.
*/
function freezeObjectIfDev<T extends object>(value: T) {
if (!__DEV__) {
return;
}
Object.entries(value).forEach(([key, element]) => {
Object.defineProperty(value, key, {
get() {
return element;
},
set() {
console.warn(
`[Reanimated] Tried to modify key \`${key}\` of an object which has been already passed to a worklet. See
https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-shareable
for more details.`
);
},
});
});
Object.preventExtensions(value);
}

export function makeShareableCloneOnUIRecursive<T>(
value: T
): FlatShareableRef<T> {
Expand Down