React Native state update during animation "resets" the animation - react-native

I am facing a problem that I've tried to solve in lots of different ways, but I cannot get it to work. Please see this Expo application, I've created a dumb example that demonstrates my problem: https://snack.expo.io/HJB0sE4jS
To summarize, I want to build an app with a draggable component (The blue dot in the example), but while the user drags the component I also need to update the state of the app (the counter in the example). The problem is that whenever the state updates during dragging, the component resets to it's initial position. I want to allow the user to freely drag the component while state updates happen.
I was able to "solve" the issue by putting the PanResponder in a useRef, so it won't be reinitialized in case of a state update, but as you can see in the example, I want to use the state in the PanResponder. If I put it in a useRef I cannot use the state in the PanResponder because it will contain a stale value (it will always contain the initial value of the counter which is 0).
How do you handle these kind of situations in react native? I guess it is not too uncommon that someone wants to update the state during an animation, although I cannot find any documentation or examples on this.
What am I doing wrong?
Edit: I was investigating further and I can see that the problem is that I'm mapping the (dx,dy) values from the gesture parameter to the position, but the (dx,dy) values are reset to (0,0) when the state changes. I guess (dx,dy) initialized to (0,0) when PanResponder is created. Still don't know what to do to make this work...

A mutable ref that holds the latest counter state value, along with a ref to prevent re-initializing the PanResponder should solve the problem in the example:
const [counter] = useCounter();
// Update the counterValue ref whenever the counter state changes
const counterValue = useRef(counter);
useEffect(() => {
counterValue.current = counter;
}, [counter]);
const position = useRef(new Animated.ValueXY());
const panResponder = useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: position.current.x, dy: position.current.y }],
{ listener: () => console.log(counterValue.current) } // counterValue being a ref, will not go stale
),
onPanResponderRelease: () => {
Animated.spring(position.current, { toValue: { x: 0, y: 0 } }).start();
}
})
);
You can check the above suggestion here: https://snack.expo.io/rkMLgp4jB
I understand though that this is a rather simplified example, and may not work for your actual use-case. It would help if you could share some more details on the actual usage!

Related

React Native console.log old value useState

I'm having trouble with React Native showing wrong value for me. I wan't to show the value after an useState update. My goal is to pass the value to the parent component but right now it passes the opposite value (true when switch is off). What do I have to do to console.log the right value after a useState update?
Watch image for example here
The useState hook is somewhat asynchronous (although you cannot wait for it).
Try using a useEffect:
useEffect(() => {
console.log(isEnabled)
}, [isEnabled]) // Array of dependencies: when any of these value changes, the function in the useEffect will re-run
More information here:
https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia
https://javascript.plainenglish.io/why-you-shouldnt-always-use-usestate-658994693018
The Change function will always "see" the state value that existed at the time of running the function. This is not because of asynchronicity per se (state updates are actually sync) but because of how closures work. It does feel like it is async though.
The state value will properly update in the background, but it won't be available in the "already-running" function. You can find more info here.
The way I see your handler implemented though:
const handleChange = () => {
setIsEnabled(!isEnabled) // you do not need updater function, you can directly reference the state
triggerParentMethod(!isEnabled); // then you can also directly call the parent function here
}
I recommend this as this way you will notify the parent immediately on user click instead of waiting for the state to be set and then notifying the parent in the next render cycle (in the effect), which should be unnecessary.
State updates in React are asynchronous, meaning that React does not wait for the state to be updated before executing the next line of code. In your case, the state update setIsEnabled(...) is not finished before console.log(isEnabled) is run, and therefore it returns the old value.
Just put the console.log(isEnabled) outside the function for it to print the update correctly. The component SetupSwitch is re-rendered when the state isEnabled is updated, which means it prints the console.log of the updated variable again.
...
console.log(isEnabled);
const Change = () => {
...
You will have to implement useEffect to view the changes.
useState is an asynchronous function it will go to the callback queue, meanwhile, the value will be consumed, so you need to trigger the action whenever the count changes. (for this example)
const [count, setCount] = useState(0);
useEffect(() => console.log(count), [count]);
setCurrPos(preevCount => prevCount + 1);

useFocusEffect runs every time useState updates

I've been trying to use useState inside useFocusEffect. Following react-navigation I got something like this:
const [counter, setCounter] = useState(0);
useFocusEffect(
useCallback(() => {
console.log(counter);
}, [counter]),
);
Now the problem is that every time counter updates, useFocusEffect fires. What I want is for it to fire only when screen comes into focus. Now I've also tried doing this with navigation focus listener:
useEffect(() => {
const onFocus = navigation.addListener('focus', () => {
console.log(counter);
});
return onFocus;
}, [navigation, counter]);
It works, well partially. While the onFocus function is performed only when screen comes into focus, useEffect fires every time counter updates. Same thing happens when using redux-toolkit slices. How can I prevent this behaviour?
Update
I should add that removing counter from dependency array prevents it from updating in subsequential runs. So I will rephrase the question. Is there a way to either fix useCallback by preventing it from firing every time counter updates or fix useEffect so that it fires only on focus with counter updated?
You included counter in the dependency array of your useEffect. This tells the useEffect to run every time a change is made to counter. See the docs for useEffect here: https://reactjs.org/docs/hooks-reference.html#useeffect
You need to change your useEffect to this:
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
console.log(counter);
});
return unsubscribe;
}, [navigation]); // remove `counter` from your dependency array
The problem you had was your counter value was in the dependency array of your useEffect dependency array. This means that every time your counter value changes the callback inside your useEffect will run which is obviously undesirable.
You most likely followed CRA's default linting rules where they give you a warning to add items to the dependency array, I'd highly recommend turning that off in your linting rules.

track UI elements states with one object, but the states are not reserved once leaving the screen and coming back

In my react-native project, I have three checkboxes, I need to track the state of those checkboxes so I use an object with key-value (value is boolean) to represent the states of all three checkboxes and use useState hook to manage them. Here is my code:
import React, { useState, useEffect } from 'react';
...
const MyScreen = ({ navigation }) => {
// initially, all checkboxes are checked
const initialCheckBoxState = {
0: true,
1: true,
2: true,
};
const [checkBoxesState, setCheckBoxesState] = useState(initialCheckBoxState);
useEffect(() => {
return () => {
console.log('Screen did unmount');
};
}, [checkBoxesState]);
return (
<View>
...
<SectionList
sections={options}
renderItem={({ index, item }) => (
<CheckBox
onPress={() => {
const checkBoxesStateCopy = { ...checkBoxesState };
checkBoxesStateCopy[index] = !checkBoxesStateCopy[index];
setCheckBoxesState(checkBoxesStateCopy);
}}
/>
)}
/>
...
</View>
);
};
I omitted code that is not the concern of my problem. As you can see, for each item I draw one CheckBox component.
In practice, there are always three items (i.e. three check boxes to show). At the beginning I declared initialCheckBoxState, each key-pair represents the state of the checkbox of each. In the onPress callback of Checkbox I toggle each check box state & update the checkBoxesState by hook method setCheckBoxesState as a whole.
Everything works fine at runtime, my screen is re-rendered when toggling checkbox state, UI shows the status of checkboxes correctly. But issue comes when I navigate back to the previous screen and navigate back to this screen, all checkboxes states are back to the initial states.
So, why the checkboxes states are not reserved?
P.S. previous screen and MyScreen are under the same stack navigator. User press a button of previous screen to navigate to MyScreen. From MyScreen user can go to previous screen by pressing the "headerLeft" button
First lets answer the question:
why the checkboxes states are not reserved?
This component is handling its state completely independent, the state is created & handled inside and no values are passed-in from outside. what does it mean? this component has its initial state value inside of itself, it doesn't use any prop or anything else to initialize the state. everytime this component gets created, state is again initialized with that value. so that's the reason you lose all changes done to checkboxes, because when you leave this screen(component) , it gets unmounted(we'll talk about this in next question) and because all values are just handled inside, every data (containing checkboxes state) will be lost.
So now lets talk about this:
is react-native supposed to reserve the state when come back to the screen?
short answer is No. Every component is destroyed when unmounted including their state and data.
Now lets answer why
screens are still on the stack in memory, not destroyed?
Usually developers use a package like react-navigation or RNRF(which is built on top of react-navigation) for react navigation, most of times we don't care about how they handle this navigation logic, we just use the interface the provided us. each of these packages may have their own way to handle navigation. providing full answer to determine why exactly the screen in still in memory needs full code review and sure lots of debugging but i guess there are 2 possibilities. first as i said maybe the package you are using keeps the unmounted screens in memory at least for a while for some reason. the 2nd is a common react community issue which is Unmounted component still in memory which you can check at: https://github.com/facebook/react/issues/16138
And at last lets answer the question:
how do i keep checkboxes state even with navigating back and losing component containing their state?
This doesn't have just one way to that but simple and short answer is move your state out of the that component, e.g move it out to the parent component or a global variable.
to make it more clear lets explain like this: imagine screen A is always mounted, then you go in B and there you can see some checkboxes and you can modify the states. if the state is handled completely inside B, if you navigate back from screen B to A you lose all changes because B is now unmounted. so what you should do it to put checkboxes states in A screen then pass the values down to B. and when modifying the values, you modify A state. so when B gets unmounted all changes are persistant because you have them in A.
other approached exists as well, you can create a global singleton object named globalState. then put values needed to share between multiple screens there. if you prefer redux or mobx you can use them. one of their usages is when you have some data that you need to share between mutiple screens, these data are independent from where you are at and will persist.
This explanation is from official react-navigation documentation:
Consider a stack navigator with screens A and B. After navigating to
A, its componentDidMount is called. When pushing B, its
componentDidMount is also called, but A remains mounted on the stack
and its componentWillUnmount is therefore not called.
When going back from B to A, componentWillUnmount of B is called, but
componentDidMount of A is not because A remained mounted the whole
time.
https://reactnavigation.org/docs/navigation-lifecycle/#example-scenario
Your MyScreen screen is equivalent to screen B from the example, which means you can expect your screen to stay mounted if you navigate forward, but not backwards.
Its simple, just add a keyExtractor to your SectionList component, which would uniquely identify each checkbox, so that react knows which one to re-render on update.
You'll want to use AsyncStorage to persist data to the device. State variables will be cleared any time the component unmounts.
AsyncStorage docs:
https://react-native-community.github.io/asaync-storage/
import AsyncStorage from '#react-native-community/async-storage';
//You can only store string values so convert objects to strings:
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('#storage_Key', jsonValue)
} catch (e) {
// saving error
}
}
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#storage_Key')
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch(e) {
// error reading value
}
}
UPDATE -
State is not being persisted due to the nature of React Component lifecycles. Specifically, when you navigate away from a screen the lifecycle method componentWillUnmount is called.
Here's an excerpt from the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
...Once a component instance is unmounted, it will never be mounted again.
This means any values stored in state will be destroyed as well and upon navigating back to the screen ComponentDidMount will be called which is where you may want to assign persisted values back to state.
Two possible approaches aside from AsyncStorage that may work for some use cases to persist data across screens is using Context or a singleton.

How to update toValue for a spring animation in React Native (Animated API)?

I'd like to be able to change the toValue of an animation responding to props update. The official docs for React Native Animated API state that the spring method
animates a value according to an analytical spring model based on damped harmonic oscillation. Tracks velocity state to create fluid motions as the toValue updates, and can be chained together.
However, I haven't found anywhere how we can update toValue. Basically, my component looks like this:
const ProgressBar = ({ loadPercentage }) => {
const loadAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
animation.current = spring(loadAnim, {
toValue: loadPercentage,
}).start();
}, [loadAnim, loadPercentage]);
....
}
This doesn't work for all cases. In particular, if loadPercentage changes too often, the component takes up a huge amount of resources. This kinda makes sense, since I'm creating a new animation for each update. Instead, I'd like to simply modify toValue without starting a new animation or anything like that.
This seems pretty basic, but after 4 hours of trying stuff/googling, I give up. -.-
Just in case, I also tried using react-native-reanimated, but no luck there either.

React Native VirtualizedList get Scroll and scroll to position

So i am on RN 49.3,
I am looking for a way to get the scroll position or the index of the item visible on the virtualizedList!
PS: when i opened the source code of VirtualizedList.js which seems to have props.onScrollEndDrag & props.onScroll
We were using a different approach. It has changed a lot since, but I can help you with the initial approach. We added onScroll param to the list. We captured event.nativeEvent.contentOffset.y into the state or some variable inside spec. We used redux. When the user left the screen we saved this value inside a database. Second part was to load this value from the db in componentDidMount . You just put a ref into the list component and then call this.refs.myRef.scrollTo({ x: 0, y: loadedValue, animated: false });
Capture the scroll
render() {
return (
<VirtualizedList
ref='myRef'
onScroll={event => {
this.scroll = event.nativeEvent.contentOffset.y;
}}
...
/>
);}
Save on exit
componentWillUnmount() {
AsyncStorage.setItem(key, this.scroll);
}
Load after mounting
componentDidMount() {
AsyncStorage.getItem(key)
.then(y => {
this.refs.myRef.scrollTo({ x: 0, y, animated: false });
});
}
Best approach I think is to handle it inside redux and you connect this component with the list to the store. I mentioned saving into db because we do save the position for later use, this is only optional and depends on your requirements.
You may use the state as well, but then you need to handle the unnecessary updates