React native reanimated runOnJs - does not update state every time - react-native

I have a list of items that should change state when they are swiped passed a certain threshold. I'm using runOnJs to call a function to change the state. Now when I swipe an item the first time, it updates it's state but every swipe after that does nothing. Can someone please explain to me what I'm missing here?
let [cleaned, setCleaned] = useState(false);
let handleCleanPress = () => {
console.log(clean);
setCleaned(!cleaned);
translateX.value = withTiming(0);
};
let panGesture = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onStart: (_, context) => {
context.startX = translateX.value;
},
onActive: (event, context) => {
let start = context.startX + event.translationX;
if (start < 0) {
translateX.value = start;
}
},
onEnd: () => {
let shouldTriggerClean = translateX.value < translateXThreshold;
translateX.value =
translateX.value >= snapThreshold && translateX.value < -BUTTON_WIDTH
? withTiming(snapPoint, { duration: 200 })
: withTiming(0, { duration: 200 });
if (shouldTriggerClean) {
runOnJS(handleCleanPress)();
}
},
});

Feels a bit wrong doing it like this but it works. Maybe someone can suggest a better way or confirm this is correct?
let setCleanState = () => {
setCleaned(!cleaned);
};
let handleCleanPress = () => {
translateX.value = withTiming(0, { duration: 200 }, (finished) => {
if (finished) {
runOnJS(setCleanState)();
}
});
};

I think part of the problem here may be that you're mixing the "JS in UI Thread"("worklets", translateX.value) with the "Main React Native JS Thread"(setState).
Read more about that [here][1].
You fixed that in your follow-up comment by only using runOnJS on setCleanState. Which I think is why it was working, albeit not reliably.
Did you also remove the withTiming functions in your onEnd() after your comment?
[1]: https://docs.swmansion.com/react-native-reanimated/docs/#:~:text=interactions%20and%20animations,the%20UI%20thread).

Related

undefined is not an object (evaluating 'fun.__callAsync') trying to use setTimeout in a component

I'm using React Native and Reanimated and I want an animation to play after 2 seconds.
When a user moves a card, the card should stay at it's new position for 2 seconds, and then move back to it's place.
This is what I have:
const panGesture = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onActive: event => {
translateX.value = event.translationX;
if (event.translationX <= 0) {
// disabling swiping from right to left
translateX.value = 0;
}
},
onEnd: event => {
const shouldStick = translateX.value >= TRANSLATE_X_THRESHOULD;
if (shouldStick) {
translateX.value = withTiming(120);
runOnJS(moveBack)(translateX);
} else {
translateX.value = withTiming(0);
}
},
});
I tried using setTimeOut to count 2 seconds, and then update translateX but I get this error:
undefined is not an object (evaluating 'fun.__callAsync')
This is moveBack function:
const moveBack = (translateX: Animated.SharedValue<number>) => {
console.log("TRANSLATEX: " + translateX);
setTimeout(() => {
translateX.value = 0;
}, 2000);
}
I don't even see the TRANSLATEX log, so I guess it won't even get there.
I can't really figure out what's the problem or how to word it so I can find a solution.
The solution was way easier than I thought.
I'm using Reanimated 2.2.0 and there is withDelay option to add to the animation and it works great.
This is what I added after translateX.value = withTiming(120); (instead of the runOnJs line):
translateX.value = withDelay(2000, withTiming(0));
So right after setting translateX to 120, it waits 2 seconds, and then setting the value back to 0.
when using runOnJS
const doSomething = () => {
...
} // declare arrow function before runOnJS
runOnJS(doSomething)(arg)
function doSomething(){
...
} // can declare normal function before/after runOnJS coz of hoisting

react-native-reanimated how to use delay between 'Animated.timing' functions

I use react-native-reanimated version: '1.7.1' and I tried to process delay between 4 different timing functions.
I tried to find instructions on the web and didn't find one that was clear:
https://docs.swmansion.com/react-native-reanimated/docs/1.x.x/about#reanimated-overview
https://docs.swmansion.com/react-native-reanimated/docs/1.x.x/declarative
I know that in the original reactNative API there is a delay so I tried to find something comparable with this good library
export const createTimingAnimation = (value: Animated.Node<number>, duration = 500, easing = Easing.inOut(Easing.ease), toValue = 1) => {
return Animated.timing(value, {
toValue,
duration,
easing,
});
};
I didn't find any formal way, so I created one:
export const timingAnimationWithDelay = (delay: number, timingAnimation: Animated.BackwardCompatibleWrapper, finishCallback?: any): void => {
setTimeout(() => {
timingAnimation.start(() => {
finishCallback && finishCallback();
});
}, delay);
};
And then you call it like this:
const greetingNfnTiming = createTimingAnimation(animatedValue, 480, Easing.out(Easing.cubic));
timingAnimationWithDelay(1000, greetingNfnTiming, onGreetingFinish);

(AppsFlyer / ReactNative) How can I get attribution parameter from onAppOpenAttribution?

This might be a dumb question, but currently I really need a help. Can someone please help me out?
I'm implementing AppsFlyer on my ReactNative Project (Android)
What I want to do is console.log attribution parameter.
But, there are no console.logging happening.
Could someone please read my snippet and how can I access to attribution parameter, please?
or, is there any proper way to console.log attribution parameter or save it to variable?
App.tsx
​import appsFlyer from 'react-native-appsflyer';
var testFunc = appsFlyer.onAppOpenAttribution(
    (data) => {
        console.log(data);
    }
);
appsFlyer.initSdk(
    {
        devKey: '***************************',
        isDebug: false,
    },
    (result) => {
        console.log(result);
    },
    (error) => {
        console.error(error);
    },
);
const Home: React.FC<Props> = props => {
    const [appState, setAppState] = useState(AppState.currentState);
    // ! when I press device's home button (appstate changes to background),
   // ! console.log in testFunc is not working...
  
    useEffect(() => {
        function handleAppStateChange(nextAppState) {
            if (appState.match(/active|foreground/) && nextAppState === 'background') {
                if (testFunc) {
                    testFunc();
                    testFunc = null;
                }
            }
          setAppState(nextAppState);
       }
        AppState.addEventListener('change', handleAppStateChange);
        return () => {
        AppState.removeEventListener('change', handleAppStateChange);
      };
  })
To my understanding, the onAppOpenAttribution event only triggers when you already have the app installed and click on a deep link. Try to use onInstallConversionData instead and see what happens, since it triggers once the SDK is initialized. I'd also remove the "useEffect" section entirely just to test. I hope this helps.
nevermind,
I added appsFlyer.onInstallConversionData
then it worked...
import appsFlyer from 'react-native-appsflyer';
var onInstallConversionDataCanceller = appsFlyer.onInstallConversionData((res) => {
if (JSON.parse(res.data.is_first_launch) == true) {
if (res.data.af_status === 'Non-organic') {
var media_source = res.data.media_source;
var campaign = res.data.campaign;
console.log('This is first launch and a Non-Organic install. Media source: ' + media_source + ' Campaign: ' + campaign);
} else if (res.data.af_status === 'Organic') {
console.log('This is first launch and a Organic Install');
}
} else {
console.log('This is not first launch');
}
});
var onAppOpenAttributionCanceller = appsFlyer.onAppOpenAttribution((res) => {
console.log(res)
});
appsFlyer.initSdk(
{
devKey: '***************************',
isDebug: false,
},
(result) => {
console.log(result);
},
(error) => {
console.error(error);
},
);
const Home: React.FC<Props> = props => {
const [appState, setAppState] = useState(AppState.currentState);
useEffect(() => {
function handleAppStateChange(nextAppState) {
if (appState.match(/active|foreground/) && nextAppState === 'background') {
if (onInstallConversionDataCanceller) {
onInstallConversionDataCanceller();
onInstallConversionDataCanceller = null;
}
if (onAppOpenAttributionCanceller) {
onAppOpenAttributionCanceller();
onAppOpenAttributionCanceller = null;
}
}
AppState.addEventListener('change', handleAppStateChange);
return () => {
AppState.removeEventListener('change', handleAppStateChange);
};
})

React Native - start Animated twice, an error start of undefined

animation = new Animated.Value(0);
animationSequnce = Animated.sequence(
[Animated.timing(this.animation, { toValue: 1 }),
Animated.timing(this.animation, { toValue: 0, delay: 1000 }),
],
);
startAnimation = () => {
animationSequnce.start();
}
stopAnimation = () => {
animationSequnce.stop();
}
I want to start an animation sequence several times.
I tested it by writing code that calls startAnimation when the button is pressed.
The animation runs on the first run.
When the second button is clicked after the first animation is finished
Cannot read property 'start' of undefined error occurs.
startAnimation = () => {
Animated.sequence(
[Animated.timing(this.animation, { toValue: 1 }),
Animated.timing(this.animation, { toValue: 0, delay: 1000 }),
],
).start();
}
This change to startAnimation will not cause an error, but you will not be able to call stopAnimation because it will call a different AnimationSequnce each time.
What is the best way to use an animation multiple times?
to stop animation
this.animation.stopAnimation()
optional get callBack when the animation stop
this.animation.stopAnimation(this.callback())
or
Animated.timing(
this.animation
).stop();
The first time you call Animated.sequence(), react native creates a variable current, refers to the index of the current executing animation, and stores it locally.
When Animated.sequence().start() finished, the variable current won't be cleaned or reset to 0, so the second time you call start() on the same sequenced animation, the array index out of bound and cause error.
It's a hidden bug of react native, below is the implementation of Animated.sequence
const sequence = function(
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function(callback?: ?EndCallback) {
const onComplete = function(result) {
if (!result.finished) {
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function() {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function() {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function() {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function(): boolean {
return false;
},
};
};
My solution is to define a custom sequence, and manually reset current to 0 when the sequenced animations finished:
const sequence = function(
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function(callback?: ?EndCallback) {
const onComplete = function(result) {
if (!result.finished) {
current = 0;
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
current = 0;
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
current = 0;
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function() {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function() {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function() {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function(): boolean {
return false;
},
};
};

Wait until API fully loads before running next function -- async/await -- will this work?

I am a beginner with Javascript with a bit of knowledge of VueJs. I have an array called tickets. I also have a data api returning two different data objects (tickets and user profiles).
The tickets have user ids and the user profiles has the ids with names.
I needed to create a method that looks at both of that data, loops through it, and assigns the full name of the user to the view.
I was having an issue where my tickets object were not finished loading and it was sometimes causing an error like firstname is undefined. So, i thought I'd try and write an async/await approach to wait until the tickets have fully loaded.
Although my code works, it just doesn't "feel right" and I am not sure how reliable it will be once the application gets larger.
Can I get another set of eyes as to confirmation that my current approach is OK? Thanks!
data() {
return {
isBusy: true,
tickets: [],
userProfiles: [],
}
},
created() {
this.getUserProfiles()
this.getTickets()
},
methods: {
getUserProfiles: function() {
ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
ApiService.getTickets().then(response => {
this.tickets = response.data
this.assignNames(this.tickets)
this.isBusy = false
})
},
// lets wait until the issues are loaded before showing names;
async assignNames() {
let tickets = await this.tickets
var i
for (i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}
}
</script>
There are several ways you could do this. Here is the one I prefer without async/await:
created() {
this.load();
},
methods: {
getUserProfiles: function() {
return ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
return ApiService.getTickets().then(response => {
this.tickets = response.data
})
},
load() {
Promise.all([
this.getUserProfiles(),
this.getTickets()
]).then(data => {
this.assignNames();
this.isBusy = false;
});
},
assignNames(){
const tickets = this.tickets;
for (let i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}