How can I tell my counter to stop incrementing when it has reached a specific amount? - react-native

I'm trying to implement a counter that stops when it has reached a certain amount, but what I have at the moment doesn't stop. It seems to increment continuously; why is this? Does it have something to do with when or how many times useEffect is executed?
const [counter, setCounter] = useState(0);
useEffect(() => {
if (counter < 10) {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, incrementRate);
return () => clearInterval(interval);
}
}, []);
return (
<View>
<Text>Circular progress bar</Text>
<Text>{`${counter}`}</Text>
</View>
);

You're using the useState functional updater inside your effect, which means it will use its previous state to increment the count, which is correct but it gives you a false perception that the effect is working correctly. Your mental model for how useEffect works needs to a shift a little, it's really common, so much so Dan Abramov has written an extensive article on useEffect with this exact scenario.
The quick fix is to add counter to your dependency array, so that you can tell React its changed, so it will no longer skip updating the effect.
useEffect(() => {
if (counter < 10) {
const interval = setInterval(() => {
setCounter((counter) => counter + 1);
}, incrementRate);
return () => {
clearInterval(interval);
};
}
}, [counter]);
It's not the cleanest approach as you'll clear the interval and create a new one each time the effect runs.
You could however use setTimeout instead and avoid the clearing of intervals altogether.
useEffect(() => {
if (counter < 10) {
setTimeout(() => {
setCounter((counter) => counter + 1);
}, incrementRate);
}
}, [counter]);

Related

React native reanimated runOnJs - does not update state every time

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).

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

Reset the initial state value in Recat Native

Requirement: I have to blink a View for 2 sec with 2 different color (for ex red and white).
I can do this by using this code -
const [state, setState] = React.useState(false)
const [initialState, setInitialState] = React.useState(0)
React.useEffect(() => {
if (initialState < 2){
let interval = setInterval(() => {
setState(true)
setInitialState(initialState + 1)
setTimeout(() => {
setState(false)
}, 80);
}, 300);
setTimeout(() => {
clearInterval(interval)
}, 600);
}
}, [initialState])
and called it like -
<View style={{...styles.mainContainer, backgroundColor: state ? Colors.GRO7 : Colors.GRC9}}>
Another Requirement: I have another screen from it i an change the address, on successful address change i have to blink this view again for 2 sec. I'm not sure where i can reset the initial value to 0 again.
I am new In react native, could some one guide me how to achieve this functionality
Can‘t completely understand what‘s your target.
Here is a blinking text sample
you can pass in the initial value to this component instead of defining 0 in this line:
const [initialState, setInitialState] = React.useState(0)
you could have a param to pass in and put it instead of 0 so that every time that param changes this component will re render. and so you get a new initial state.
for example:
const [initialState, setInitialState] = React.useState(initialValue)

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);

Is there a cleaner way to organize this recoilJS state?

I've just learned about recoilJS and have been playing around with it a bit and have a question about whether what I've done is considered "correct." My code works, but it feels weird.
I've got the following React function component:
export const TimerPanel: FC = () => {
const gameState = useRecoilValue<GameState>(HeaderAtoms.gameState);
const timerCount = useRecoilValue(HeaderAtoms.timerCount);
const setTimerCounter = useSetRecoilState(HeaderAtoms.timerCounter);
useEffect(() => {
if (gameState === GameState.IN_PROGRESS && timerCount < 1000) {
window.setTimeout(() => {
setTimerCounter(timerCount + 1);
}, 1000);
}
});
return <NumberPanel num={timerCount} />;
};
where the relevant atom and selector are defined as:
export const timerCount = atom<number>({
key: 'Header.timerCount',
default: 0
});
export const timerCounter = selector<number>({
key: 'Header.timerCounter',
get: ({ get }) => {
return get(timerCount);
},
set: ({ get, set }, newCount) => {
if (get(gameState) === GameState.NEW) {
set(timerCount, 0);
} else if (get(gameState) === GameState.IN_PROGRESS) {
set(timerCount, newCount);
}
}
});
Basically, when the game starts, the TimerPanel increments the timer display by 1 every second the game is in progress. If the user resets the game (GameState.NEW), timerCount resets back to zero. If the atom/selector aren't done properly, there's a race condition in that the game state and timer count will reset, but the timer is still going and will still update the TimerPanel one more time. This is why I have the if blocks in my selector's set prop.
Basically, I'm concerned that my timerCounter selector is a glorified filter/pass-thru entity for the timerCount state and am wondering if there's a better way to handle this use case.
If your concern is the race condition, in your code, I don't see how your timer would click. setTimeout will tick once.
Anyway, you have 2 states, game state and timer state. You want to reset timer when game state changes. How about doing it in a game state selector? or move the logic into a custom hook and operate on these 2 states directly.