If worklet calls callback function - only body of this function is called. Functions scheduled for the next tick (promise) are not called (until some manul event)
const callWithPromise = () => {
return Promise.resolve();
};
const Row = () => {
return <View style={styles.row} />;
};
const Application = () => {
const onSwipeCallback = useCallback(() => {
console.log('[onSwipeCallback] was called');
callWithPromise()
.then(() => {
console.log('[onSwipeCallback] promise then');
})
.catch(() => {
console.log('[onSwipeCallback] promise catch');
});
}, []);
return (
<View style={styles.container}>
<SwipableRow onSwipeLeft={onSwipeCallback} onSwipeRight={onSwipeCallback}>
<Row />
</SwipableRow>
</View>
);
};
This callback code is not working as expected. then callback should be called immediately, but it's not.
Promise should be scheduled and executed immidiately.
Promise is not executed until some external event (touch screen)
SwipableRow.txt
Application.txt
@terrysahaidak @kmagiera @jakub-gonet Can anyone help?
For me it's a blocker because any interaction with database (using TypeORM) is no more working as TypeORM provides Promise-based API and I can't find a way for workaround.
Also I've found there is a more foundational problem which can be illustrated using setImmediate.
...
const onSwipeCallback = useCallback(() => {
console.log(`onSwipeCallback: start function`);
setImmediate(() => {
console.log(`onSwipeCallback: setImmediate function`);
});
}, []);
...
const gestureHandler = useAnimatedGestureHandler({
onEnd: (event) => {
...
positionX.value = withTiming(
toValue,
{
duration: 250,
easing: Easing.out(Easing.ease)
},
(isCancelled) => {
onSwipeCallback();
}
);
}
});
Here setImmediate should be scheduled to executed immidiately, but it's not. As Promise is based on setImmediate I think this is a root cause of a bug.
I can verify it's not working by this code:
export default function App() {
const value = useSharedValue(0);
const p = () => {
console.log('end');
const cb = () => console.log('promise');
(() => Promise.resolve(() => cb()))();
};
const handler = useAnimatedGestureHandler({
onActive: (evt) => {
value.value = withTiming(1, undefined, () => {
p();
});
},
});
return (
<View style={{ margin: 40 }}>
<TapGestureHandler numberOfTaps={1} onGestureEvent={handler}>
<Animated.View>
<Text>Test</Text>
</Animated.View>
</TapGestureHandler>
</View>
);
}
Hey
@likern I ran this code on rea2 playground using react native 0.62.2. Those logs:
[onSwipeCallback] was called[onSwipeCallback] promise then@terrysahaidak is your code 100% valid? If I understand correctly as for those lines:
const cb = () => console.log('promise');
(() => Promise.resolve(() => cb()))();
You want cb to be called eventually? If so this code is not ok because you call a function which returns a promise that resolves to a function which calls cb and returns it's result. It's all ok but there is that promise on the way, shouldn't it look like that?
(() => Promise.resolve(() => cb()))().then((result) => result());
This example you provided wouldn't work properly on a bare JS VM. I might get something wrong however.
I will try to reproduce the problem.
So I was able to reproduce the problem on Android using code provided by @likern. On iOS it works alright.
Hey
@likern I ran this code on rea2 playground using react native 0.62.2. Those logs:
[onSwipeCallback] was called[onSwipeCallback] promise then
are called respectively, immediately after swiping the container. Tested on iOS.@terrysahaidak is your code 100% valid? If I understand correctly as for those lines:
const cb = () => console.log('promise'); (() => Promise.resolve(() => cb()))();You want
cbto be called eventually? If so this code is not ok because you call a function which returns a promise that resolves to a function which callscband returns it's result. It's all ok but there is that promise on the way, shouldn't it look like that?
(() => Promise.resolve(() => cb()))().then((result) => result());
This example you provided wouldn't work properly on a bare JS VM. I might get something wrong however.I will try to reproduce the problem.
I can't test on iOS as own only Android phone. But it's reproducible on Android (I've used real Xiaomi MI 9T Pro device).
Neither Proxy, nor setImmediate were working.
Oh, yes you're right @karol-bisztyga, my code is not working
I've got this one working on alpha.5 on both iOS and Android:
export default function App() {
const value = useSharedValue(0);
const p = () => {
return Promise.resolve();
};
const cb = () => {
p().then(() => console.log('resolved'));
};
const handler = useAnimatedGestureHandler({
onActive: (evt) => {
value.value = withTiming(1, undefined, () => {
cb()
});
},
});
return (
<View style={{ margin: 40 }}>
<TapGestureHandler numberOfTaps={1} onGestureEvent={handler}>
<Animated.View>
<Text>Test</Text>
</Animated.View>
</TapGestureHandler>
</View>
);
}
But it crashes when I use p().then(() => console.log('resolved')); directly inside the callback of withTiming.
We have encountered this problem before. @likern if you want a quick hacky walkaround, here it is:
const callWithPromise = () => {
//return Promise.resolve();
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 0);
});
};
We will try to come up with a decent solution anyway.
We have encountered this problem before. @likern if you want a quick hacky walkaround, here it is:
const callWithPromise = () => { //return Promise.resolve(); return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 0); }); };We will try to come up with a decent solution anyway.
@karol-bisztyga This doesn't work for me. I have that code inside callback
console.log(`[onTaskArchive] called with taskId ${id}`);
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`[onTaskArchive] setTimeout was called`);
resolve(10);
}, 0);
});
promise.then((value: number) => {
console.log(`[onTaskArchive] promise.then called with taskId ${value}`);
});
On swipe I see only [onTaskArchive] called with taskId 6c77e35d-f465-4232-885d-f3ecf0fcb47e. Neither Promise constructor, nor .then() are executed until I press on screen again.
Also Promise.resolve() needed only for reproducing bug. In real code I need to call external TypeORM API repository.remove(user) which returns Promise.
It's still a major blocker for me right now.
Even just setTimeout (without Promise) not working.
console.log(`[onTaskArchive] called with taskId ${id}`);
setTimeout(() => {
console.log(`[onTaskArchive] setTimeout was called`);
}, 0);
setTimeout, setImmediate and Promise are all broken.
Did you wrap it inside the callback? So you should pass callback defined outside the worklet.
Did you wrap it inside the callback? So you should pass callback defined outside the worklet.
@terrysahaidak Yes, I created usual js function as callback and pass it to component.
const callback = useCallback(() => {
console.log('callback was called!');
}, []);
const onTaskArchive = useCallback(
(id: string) => {
console.log(`[onTaskArchive] called with taskId ${id}`);
setTimeout(callback, 0);
setImmediate(callback);
},
[taskRepository]
);
...
<ui.Task
...
onArchiveSwipe={onTaskArchive}
onCompletionSwipe={onTaskCompletion}
/>
@terrysahaidak @karol-bisztyga Looks like any callback inside original callback is not working.
const anotherFunc = useCallback(() => {
console.log('anotherFunc was called');
}, []);
const cbOnArchiveSwipe = useCallback(() => {
console.log('[cbOnArchiveSwipe] function called');
anotherFunc();
}, [anotherFunc]);
anotherFunc is not called when cbOnArchiveSwipe is executed, only on screen touch.
@terrysahaidak @karol-bisztyga Looks like any callback inside original callback is not working.
const anotherFunc = useCallback(() => { console.log('anotherFunc was called'); }, []); const cbOnArchiveSwipe = useCallback(() => { console.log('[cbOnArchiveSwipe] function called'); anotherFunc(); }, [anotherFunc]);
anotherFuncis not called whencbOnArchiveSwipeis executed, only on screen touch.
It's working fine on master. I will check example with promise though.
@likern I'm still investigating but could you try to call this function right after calling a callback.
const magic = () => {
let num = 0;
const w = setInterval(() => {
if (num === 1) {
clearInterval(w);
}
num++;
}, 0);
}
Example:
...
'worklet'
callbackWithPromise();
magic();
...
@likern I'm still investigating but could you try to call this function right after calling a callback.
const magic = () => { let num = 0; const w = setInterval(() => { if (num === 1) { clearInterval(w); } num++; }, 0); }Example:
... 'worklet' callbackWithPromise(); magic(); ...I will try.
But callback is not worklet - it's a usual JavaScript function.
the callback you're passing to animation is a worklet by default - the arrow function will be converted to worklet. the only case it won't be a worklet if you pass js function directly.
@terrysahaidak @karol-bisztyga Looks like any callback inside original callback is not working.
const anotherFunc = useCallback(() => { console.log('anotherFunc was called'); }, []); const cbOnArchiveSwipe = useCallback(() => { console.log('[cbOnArchiveSwipe] function called'); anotherFunc(); }, [anotherFunc]);
anotherFuncis not called whencbOnArchiveSwipeis executed, only on screen touch.It's working fine on master. I will check example with promise though.
Have you tested on Android? I think it's reproducible on Android only.
did you try to reproduce it on master?
@likern I'm still investigating but could you try to call this function right after calling a callback.
const magic = () => { let num = 0; const w = setInterval(() => { if (num === 1) { clearInterval(w); } num++; }, 0); }Example:
... 'worklet' callbackWithPromise(); magic(); ...
I've tested. Nothing have changed. magic() function even is not called until I touch screen.
Here is the full code https://gist.github.com/likern/857b5b092959c87d4922af7ddfae93a7
@terrysahaidak Could you give a link to npm package from master? I will try to test.
As far as I know package, generated from master by createNPMPackage.sh do not produce valid package for Android.
I couldn't use it and see https://github.com/software-mansion/react-native-reanimated/issues/1024#issuecomment-663991088 "Unsatisfied Link Error" for reanimated.so.
Might be I miss something.
I mean using example project. There is no way to use directly from GitHub right now (it's hard to link properly).
@likern It worked for me. Here is your code:
import React, { useCallback } from 'react';
import Animated, {
useSharedValue,
withTiming,
Easing,
useAnimatedStyle,
interpolate,
Extrapolate,
useAnimatedGestureHandler
} from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';
import { View, StyleSheet } from 'react-native';
export const magic = () => {
let num = 0;
const w = setInterval(() => {
if (num === 1) {
clearInterval(w);
}
num++;
}, 0);
};
const DEFAULTS = Object.freeze({
SWIPE_THRESHOLD: 0.5,
ACTIVATION_THRESHOLD: 8,
ANIMATION_DURATION: 0.05,
BORDER_RADIUS: 16,
LEFT_ICON_COLOR: 'rgba(255, 255, 255, 1)',
RIGHT_ICON_COLOR: 'rgba(255, 255, 255, 1)'
});
export const SwipableRow = ({
threshold = DEFAULTS.SWIPE_THRESHOLD,
children,
style,
onSwipeLeft,
onSwipeRight
}) => {
const rowWidth = useSharedValue(0);
const rowHeight = useSharedValue(0);
const positionX = useSharedValue(0);
const leftBorderRadius = useSharedValue(0);
const rightBorderRadius = useSharedValue(0);
const activationThresholdPassed = useSharedValue(false);
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = positionX.value;
activationThresholdPassed.value = false;
},
onActive: (event, ctx) => {
let offset = 0;
let translation = event.translationX;
let rightRadius = interpolate(
event.translationX,
[-DEFAULTS.ACTIVATION_THRESHOLD, 0],
[DEFAULTS.BORDER_RADIUS, 0],
Extrapolate.CLAMP
);
let leftRadius = interpolate(
event.translationX,
[0, DEFAULTS.ACTIVATION_THRESHOLD],
[0, DEFAULTS.BORDER_RADIUS],
Extrapolate.CLAMP
);
if (!activationThresholdPassed.value) {
if (Math.abs(event.translationX) < DEFAULTS.ACTIVATION_THRESHOLD) {
translation = 0;
rightRadius = 0;
leftRadius = 0;
} else {
// @ts-expect-error
activationThresholdPassed.value = true;
}
}
positionX.value = ctx.startX + translation + offset;
rightBorderRadius.value = rightRadius;
leftBorderRadius.value = leftRadius;
},
onEnd: (event) => {
const moveDirection = Math.sign(event.velocityX);
const impulse = event.velocityX * DEFAULTS.ANIMATION_DURATION;
const possibleProgress = interpolate(
positionX.value + impulse,
[-rowWidth.value, rowWidth.value],
[-1, 1],
Extrapolate.CLAMP
);
const swipeThresholdPassed = Math.abs(possibleProgress) > threshold;
let toValue = 0;
if (swipeThresholdPassed) {
if (moveDirection === 0) {
toValue = Math.sign(possibleProgress) * rowWidth.value;
} else {
if (positionX.value > 0 && moveDirection > 0) {
toValue = moveDirection * rowWidth.value;
} else if (positionX.value < 0 && moveDirection < 0) {
toValue = moveDirection * rowWidth.value;
}
}
}
positionX.value = withTiming(
toValue,
{
duration: 250,
easing: Easing.out(Easing.ease)
},
(isCancelled) => {
// console.log(
// `Animation completed with toValue: ${toValue}, rowWidth: ${rowWidth.value}`
// );
if (toValue === 1 * rowWidth.value) {
onSwipeRight && onSwipeRight();
magic();
} else if (toValue === -1 * rowWidth.value) {
onSwipeLeft && onSwipeLeft();
magic();
}
}
);
}
});
const foregroundStyle = useAnimatedStyle(() => {
return {
borderTopLeftRadius: leftBorderRadius.value,
borderBottomLeftRadius: leftBorderRadius.value,
borderTopRightRadius: rightBorderRadius.value,
borderBottomRightRadius: rightBorderRadius.value,
transform: [{ translateX: positionX.value }]
};
});
const backgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor:
positionX.value === 0
? 'transparent'
: positionX.value > 0
? 'rgba(45, 154, 252, 0.5)'
: 'rgba(252, 119, 110, 1)'
};
});
const onLayoutCallback = useCallback(
({ nativeEvent }) => {
const layoutWidth = nativeEvent.layout.width;
const layoutHeight = nativeEvent.layout.height;
rowWidth.value = layoutWidth;
rowHeight.value = layoutHeight;
},
[rowWidth, rowHeight]
);
return (
<View style={styles.container}>
<Animated.View style={[styles.background, backgroundStyle]} />
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
style={[style, styles.foreground, foregroundStyle]}
onLayout={onLayoutCallback}
>
{children}
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: { flexDirection: 'row' },
background: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
alignItems: 'center'
},
foreground: {
width: '100%',
overflow: 'hidden'
},
leftIcon: { position: 'absolute', left: 24 },
rightIcon: { position: 'absolute', right: 24 }
});
It's only a temporary solution I'm working on a proper one.
Most helpful comment
So I was able to reproduce the problem on Android using code provided by @likern. On iOS it works alright.