React life cicle and AsyncStorage - react-native

I'm creating an app which passes some crucial info via AsyncStorage, but now have a problem when updating it on another screen.... Let's see:
On Screen 1 :
Load data from AsyncStorage on componentDidMount
componentDidMount() {
AsyncStorage.getItem("userChats").then((value) => {
this.setState({userChats: JSON.parse(value)});
});
}
then on Screen 2 I modify userChats.....
I'll like that when coming back again to Screen 1, the changes made on Screen 2 be reflected on Screen 1 but there are NOT as componentDidMount is not trigged again...........
What's the correct way to do it?
Thanks

componentDidMount is a life-cycle method. Navigating from Screen1 to Screen2 does NOT unmount Screen1. So, when you come back from Screen 2 to Screen 1, the Screen 1 does not mounting because it was NOT unmounted. Hence, componentDidMount is not called.
Whats's the correct way of doing this?
You should use Context API. When you load from AsyncStorage, set that value to Context as well. When you update the value, write changes to both AsyncStorage and Context.
PS: The AsyncStorage may not needed. It depends on your requirement. Most probably, you will be able to achieve this only with Context API.
Please check the following snack. It is done using hooks. You can do the same using class components.
https://snack.expo.io/3L9QSqWqt
UPDATE:
If the data to be handled is too large, it is not recommended to use Context since it saves all the data in the device RAM and consuming too much RAM may result in app crash.
To do this without using context:
(1) Define a function to retrieve data from AsyncStorage.
loadData() {
AsyncStorage.getItem("userChats").then((value) => {
this.setState({userChats: JSON.parse(value)});
});
}
(2) Call it in componentDidMount.
componentDidMount() {
this.loadData()
}
(3) When navigating to the Screen2, pass a callback function as a prop to call loadData function.
this.props.navigation.navigate('Screen2', {
onGoBack: () => this.loadData(),
});
(4) Then in the Screen2, before goBack, you can do this:
await AsyncStorage.setItem('userChats', updatedData);
this.props.navigation.state.params.onGoBack();
this.props.navigation.goBack();
Then, the loadData function is called in the screen2.
PS: Since you use state to store the retrieved data from the AsyncStorage, you can also directly load them into the Context and use. But, keep in mind that, using too much of RAM may cause app crash.

Related

React Native: Didn't rerender the component after data was updated

i have two screens one where the profile information is showing and another screen to edit the information. If i entered the first screen profile it's shows me the right data from the database. Then i move to the next screen where i can change the Information everthing worked so far. But if I go back to the previous screen i still see the old data. So there is no rerendering.
But if i navigate to the other screen that screen fetched the new data. and the call getCurrentUserProfile is executed
This ist the screen with the profile information about the user.
const ProfileScreen = props => {
const [userObj, setUserObj] = useState({});
useEffect(() => {
let mounted = true;
getCurrentUserProfile().then(user => {
if (mounted) {
setUserObj(user);
}
console.log(user)
});
return () => mounted = false;
}, []);
console.log("----b------") // This is only output on the first call
}
How can i fix this. Is there a way when in the database is something changed, so the component rerender and fetches the new data.
Thanks.
You are probably using #react-navigation package. So, if you want to refetch data when come back to the previous page, you can use https://reactnavigation.org/docs/use-is-focused on that “previous” page.
Just look at their docs, you will get the idea.
P.S: Usually we don't unmount components directly. In React Native, we use navigator to mount/unmount components

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 call method whenever screen loaded, Like always should call method in react native

I am developing react native project and I am loading some graphs from server response.
It is a Tab based app and this code is written in first tab.
But, In some use cases that data is not loading to that graph properly.
I have written that code in componentDidMount(), But it will call only once. But, My requirement is I have to call whenever view loaded, That time only render method is calling.
I have tried to add addlistener for navigation, But, Due to its it not navigation stack throwing error.
I have found some solution like below.
componentDidMount() {
}
fetchGraphData = () => {
//some code fetching from DB and redux based on conditions
}
render() {
this.fetchGraphData();
return (
);
}
}
But, This is not good practice as per code standards.
I am not receiving props, But, We are using some graphs which are
loading from data. My requirement is I have to call api fetch data
method after screen load every time.
Any suggestions, I have to call that fetchGraphData() once render method or view loaded.
Your problem is that when you move the 'fetchGraphData' function to a screen with the 'fetchGraphData' function, you must execute it. This problem can be solved by something simpler than I thought.
componentDidMount() {
this.fetchGraphData();
}
You can try rendering again when you move to a screen with a function.
this.props.navigation.push('functionMoveScreen') // Rendering the screen again.

React-native / redux - how to re-initialize screen via navigation?

I'm developing a react-native / redux app with a bottom-tab-navigator similar to the example at https://reactnavigation.org/docs/en/tab-based-navigation.html#customizing-the-appearance. My screens all connect to a Redux store and display shared data, however I'd like at least one of these screens to ignore the current data in the store and instead re-initialize this data each time it's navigated to (instead of continuing to display the data in whatever state it was last left in).
The screen has a method to do this, but I can't figure out how to call it after the first time the screen is rendered (e.g. from the constructor or componentDidMount() method). I can't call it from the render() method as this causes a "Cannot update during an existing state transition" error.
I need my navigator to somehow cause my HomeScreen.initializeData() method to be invoked each time the Home icon is pressed, but how do I do this?
HomeScreen.js:
initializeData() {
this.props.resetData(initialValue);
}
const initialValue = ...
(resetData() is a dispatch function that re-initializes the Redux store).
Updating state from render() would create an infinite loop. Also, you don’t want to run your state update every time the component re-render, only when the tab button is pressed. This tells me that the proper place to make your state update is some onPress function on the tab button.
So the question now relies on how to implement some onPress function on a tab button. I believe this answer this question:
Is there an onPress for TabNavigator tab in react-navigation?
So I found an answer, it's a little more complicated than might be expected: As Vinicius has pointed out I need to use the tabBarOnPress navigation option, but I also need to make my dispatch function available to this navigation option.
To do this I found I need to pass a reference to my dispatch function (which is available as a property of my screen) into the navigation option, so I've used navigation params to do this and here's what I've ended up with:
HomeScreen.js:
componentDidMount() {
initializeData(this.props);
this.props.navigation.setParams({ homeProps: this.props });
}
export const initializeData = (homeProps) => {
homeProps.resetData(initialValue);
};
const initialValue = ...
AppNavigator.js:
tabBarOnPress: ({navigation, defaultHandler}) => {
const routeName = navigation.state.routeName;
if (navigation.state.params === undefined) {
// no params available
} else if (routeName === 'Home') {
let homeProps = navigation.getParam('homeProps', null);
initializeData(homeProps);
} else if (routeName === ...
...
}
defaultHandler();
}
Notes:
I'm passing props as a navigation param rather than my dispatch function (which also works) as it's more flexible (e.g. it makes all of my dispatch functions available).
initializeData() is called both during construction of HomeScreen (for the first time the screen is displayed) and from the navigation icon (for subsequent displays of the screen).
It's necessary to check that params is defined within the navigation option as it'll be undefined the first time the screen is displayed (as screen construction has yet to occur). This also makes it necessary to call initializeData() during screen construction.

ReactNative componentDidMount doesn't get called

I have two screens: A and B, connected with a StackNavigator
Screen A is a QR code scanner. As soon as a QR code is scanned, it navigates to screen B.
In screen B, I make an API call using the QR code that gets passed as a navigation param from screen A. I trigger this API call in componentDidMount.
My issue is: if I navigate from A to B, then back to A, then to B again, componentDidMount does not get called and I have no way to trigger the API call.
EDIT:
Here's some code
Screen A
Handler function that gets called when a QR code is scanned:
handleQRCode = qrCode => {
NavigationService.navigate('Decode', {qrCode});
};
Screen B
The QR code is pulled from the navigation state params and used for an API call (startDecode) through redux.
componentDidMount() {
qrCode = this.props.navigation.state.params.qrCode;
this.props.startDecode(qrCode.data);
}
My issue is that componentDidMount only gets called the first time that route is taken.
In react-navigation each screen is kept mounted. This means that when you you go back to B, you might have changed the props, but componentDidMount was already invoked in the first creation of this screen.
There are two options available for you (AFAIK) that can handle this case:
Instead of calling this.props.navigation.navigate() you can use
this.props.navigation.push which will create another instance of
screen B, thus invoking the componentDidMount React lifecycle
event.
In screen B you can catch the event where its props have changed.
This can take place in the new static lifecycle event
getDerivedPropsFromState or it can be done in the soon to be
deprecated componentWillReceiveProps.
I was facing a similar issue and I used this.props.navigation.addListener() to resolve it. Basically, force-calling componentDidMount() may be possible by pushing same screen again using a key (I haven't tried it) but your stack will keep growing as well, which is not optimal. So, when you return to a screen already in stack, you can use addListener() to see if it is being re-focused, and you can replicate you componentDidMount() code here:
class MyClass extends Component {
someProcess = () => {
// Code common between componentDidMount() and willFocus()
}
componentDidMount() {
this.someProcess();
}
willFocus = this.props.navigation.addListener(
'willFocus',
(payload) => {
this.someProcess();
}
);
}
When MyClass is called for the first time, componentDidMount will get called. For the other times when it is still in stack but instead just gains focus, addListener will get called.
This happens because the B component is mounted only on the first time it is accessed, so componentDidMount won't be called again.
I recommend you to pass a callback to the setOnNavigatorEvent method of your navigator, with the 'didAppear' event. Your callback will be invoked on every event emitted by react-native-navigation, and you can verify to do your logic every time the screen appears (hence the use of 'didAppear' event). You can base your code on the following:
export default class ExampleScreen extends Component {
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
if (event.id === 'didAppear') {
// do API call here
}
}
}