callback function in useEffect caches state, even when dependency changes? - react-native

I've seen a couple of close questions, but none that really answered my question. I have the following code in React Native.
# activities add/edit screen
# ...
const [activities, setActivities] = useState([]);
useEffect(() => {
const _setup = async () => {
const temp = await fetch(...); // fetching data from server with await
setActivities(temp);
// building save button
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => _submit()}>
<Text style={{ color: '#007AFF', fontSize: 18, }}>
Save
</Text>
</TouchableOpacity>
)
});
setReady(true);
};
_setup();
}, []);
So I build the headerRight button in useEffect to pass it an local _submit function, which looks like this.
const _submit = async () => {
console.log(activities);
try {
// fetch to send data to server
} catch (e) {
showError(e);
}
};
There is a FlatList on this screen which gets dynamically extended based on user interaction. All is well until the user presses the save button in the header. It always loads the activities array from the last hot refresh/render. But the FlatList re-renders ok, the array gets extended just as I want it to be. I tried using "useCallback" on the _submit function and set the dependency to "activities", but still, the header button seems to call the "initial" _submit function. The only thing that helped was to split the useEffect into two separates and one handling the re-render of the button.
// first one to fetch data on initial mound
// ...
useEffect(() => {
// I am using react-navigation and react-native-screens 2.7.0
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => _submit()}>
<Text style={{ color: '#007AFF', fontSize: 18, }}>
Save
</Text>
</TouchableOpacity>
)
});
}, [activities]);
It works fine, but somehow feels kind of hacky... I was under the impression that normal functions (like the _submit) would get re-constructed with each re-render, which would be triggered with e.g. a new array-element being pushed to the activities, no? For completion, I add elements to activities like this.
const _addActivity = () => {
const temp = [...activities];
const initActivity = {
title: '', // will be set later via TextInput
startTime: new Date(),
endTime: new Date(),
}
temp.push(initActivity);
setActivities(temp);
}

Related

How to reset state when navigating away from page

I have a map that triggers a modal when I click on a marker. The modal is a touchable which navigates to another page. When the page is dismissed and the user returns to the Home page (in this case), it is not possible to touch another marker and trigger a new modal. Any idea how I should set this up so that it is reset when navigating back to home? Some code below:
constants in my functional component that set state:
const [modalVisible, setModalVisible] = useState(false);
const [selectedMarker, setSelectedMarker] = useState(false);
const markerPressed = (marker) => {
setModalVisible(true);
setSelectedMarker(marker);
};
const markerLink = (selectedMarker) => {
setModalVisible(false);
onPressListingItem(selectedMarker);
};
The marker with the param to trigger modal:
<CustomMarker
onPress={() => {
markerPressed(listing);
}}
/>
The modal with param to navigate to page:
<Modal
style={{ position: 'absolute', bottom: 10 }}
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}
setModalVisiblity={() => {
setModalVisible((preState) => (preState = !preState));
}}>
Solution 1: You could reset the data in the same place you trigger your Modal, in your markerPressed function. That way when you return, the data would be reset.
Solution 2: If you are using react-navigation, you could add a focus listener to check when your mp page comes back into focus.
navigation.addListener('focus', () => {
// reset marker state
});

ActivityIndicator while takePictureAsync with Expo Camera

I'm developing a React Native app and I want to show an ActivityIndicator component while the Expo Camera is processing the image.
<TouchableOpacity style={{alignSelf: 'center'}} onPress={takePicture}>
<FontAwesome name="camera" style={{ color: "#FEA428", fontSize: 50, marginBottom: 20}}/>
</TouchableOpacity>
So the function takePicture do this:
const takePicture = async () => {
setLoading(prevState => !prevState)
if(cameraRef){
let photo = await cameraRef.takePictureAsync({quality: 0.5, skipProcessing: true, fixOrientation: false});
setLoading(prevState => !prevState)
setPhotoAbove(photo)
}
}
setLoading change the "isLoading" prop which say if the ActivityIndicator is going to be visible or not. However, after the first setLoading(prevState => !prevState) then the following code is never executed. Am I missing something here?
EDIT: I'm posting an Expo Snack with the code for testing purposes. In Web the problem isn't visible but for example if you run it on Android it will.
https://snack.expo.io/#avradev/0a8a01
Tks.
You should move your second setLoading just after your await method call like this:
const takePicture = async () => {
setLoading(prevState => !prevState)
if(cameraRef){
let photo = await cameraRef.takePictureAsync({quality: 0.5, skipProcessing: true, fixOrientation: false});
setLoading(prevState => !prevState);
setPhotoAbove(photo);
}
}

React Native ref is undefined until page reload

I am using refs to animate a View, but the refs are throwing an undefined error. However, if I comment-out my attempt to access the ref, load the page, un-comment the code, & reload the page, the animation works fine.
My code:
export const AccountScreen = ({ navigation }) => {
return (
<SafeAreaView style={{ backgroundColor:'#FFFFFF', flex: 1 }}>
<Animatable.View style={styles.container} ref={ref => {this.containerRef = ref}}>
</Animatable.View>
</SafeAreaView>
)
};
launchOpenAnimation()
function launchOpenAnimation () {
this.containerRef.animate(appearContainerAnimation); //If I comment this out, reload, and un-comment, it works
}
What can I do to fix this? It appears that the ref isn't defined at the time that my launchOpenAnimation() is executed, but that it is defined after, thus resulting in the code working if I comment it out, reload, un-comment, and reload again.
First thing first, your question is messed up between calls component and functional component, cuz there is no this in functional component. I'll convert them to function component using useRef and useEffect
In the first run this.containerRef have not be assign to ref of Animatable yet.
Try this
export const AccountScreen = ({ navigation }) => {
const animateRef = useRef()
useEffect(() =>{
launchOpenAnimation()
function launchOpenAnimation () {
// add ? to make sure animate is called only when this.containerRef exists
containerRef?.current.animate(appearContainerAnimation);
}
},[])
return (
<SafeAreaView style={{ backgroundColor:'#FFFFFF', flex: 1 }}>
<Animatable.View style={styles.container} ref={ref => {
ref.animate(appearContainerAnimation); // add this if you need to run when initial render run
animateRef.current = ref
}}>
</Animatable.View>
</SafeAreaView>
)
};

React native updates state "on its own"

I have two screens, one list (Flatlist) and one filter screen where I want to be able to set some filters for the list. the list screen has the states "data" and "usedFilters". When I am switching to the filters screen, the states are set as navigation parameters for react navigation and then passed via navigation.navigate, together with the onChange function, as props to the filter screen. There they are read, and the filters screen class' state is set (usually with passed filters from the list screen, if no valid filters has been passed, some are initialized).
After that the filters can be changed. If that happens, the state of the filter screen gets updated.
If then the apply button is clicked the filter screens' state is passed to the onChange function and via that back to the list screen, the onChange function updates the state "usedFilters" state of the list screen. If the cancel button is pressed null is passed to the onChange function and there is no setState call.
Setting new states for the list screen works perfectly fine. the problem is, that when i press the cancel button (or the back button automatically rendered by react navigation) the changes are kept nevertheless. That only happens if the state has been changed before. So if there has never been applied a change and hence the "usedFitlers" state of the list screen is null, this behavior does not occur. Only if I already made some changes and hence the "usedFitlers" state of the list screen has a valid value which is passed to the filters screen the cancel or go back buttons won't work as expected.
I am using expo-cli 3 and tried on my android smartphone as well as the iOS simulator. Same behavior. I looked into it with chrome dev tools as well but i simply couldn't figure out where the "usedFitlers" state was updated.
I am using react native 0.60 and react navigation 3.11.0
My best guess is that for some reason the two states share the same memory or one is pointer to the other or sth like that. (Had problems like that with python some time ago, not knowing the it uses pointers when assigning variables).
Anyone got an idea?
List Screen:
export default class ListScreen extends React.Component {
state = { data: [], usedFilters: null };
static navigationOptions = ({ navigation }) => {
let data = navigation.getParam('data')
let changefilter = navigation.getParam('changeFilter')
let currfilter = navigation.getParam('currFilter')
return {
headerTitle:
<Text style={Styles.headerTitle}>{strings('List')}</Text>,
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate('FilterScreen', {
dataset: data, onChange: changefilter, activeFilters:
currfilter })} >
<View paddingRight={16}>
<Icon name="settings" size={24} color=
{Colors.headerTintColor} />
</View>
</TouchableOpacity>
),
};
};
_onChangeFilter = (newFilter) => {
if (newFilter) {
this.setState({ usedFilters: newFilter })
this.props.navigation.setParams({ currFilter: newFilter });
} // added for debugging reasons
else {
this.forceUpdate();
let a = this.state.usedFilters;
}
}
_fetchData() {
this.setState({ data: fakedata.results },
() => this.props.navigation.setParams({ data: fakedata.results,
changeFilter: this._onChangeFilter }));
}
componentDidMount() {
this._fetchData();
}
render() {
return (
<ScrollView>
<FlatList/>
// Just data rendering, no problems here
</ScrollView>
);
}
}
Filter Screen:
export default class FilterScreen extends React.Component {
static navigationOptions = () => {
return {
headerTitle: <Text style={Styles.headerTitle}> {strings('filter')}
</Text>
};
};
state = { currentFilters: null }
_onChange = (filter, idx) => {
let tmp = this.state.currentFilters;
tmp[idx] = filter;
this.setState({ currentFilters: tmp })
}
_initFilterElems() {
const filters = this.props.navigation.getParam('activeFilters');
const dataset = this.props.navigation.getParam('dataset');
let filterA = [];
let filterB = [];
let filterC = [];
if (filters) {
// so some checks
} else {
// init filters
}
const filterElements = [filterA, filterB, filterC];
this.setState({ currentFilters: filterElements })
}
componentDidMount() {
this._initFilterElems()
}
render() {
const onChange = this.props.navigation.getParam('onChange');
return (
<ScrollView style={Styles.screenView}>
<FlatList
data={this.state.currentFilters} // Listeneinträge
keyExtractor={(item, index) => 'key' + index}
renderItem={({ item, index }) => (
<FilterCategory filter={item} name={filterNames[index]}
idx={index} onChange={this._onChange} />
)}
ItemSeparatorComponent={() => <View style=
{Styles.listSeperator} />}
/>
<View style={Layout.twoHorizontalButtons}>
<TouchableOpacity onPress={() => {
onChange(this.state.currentFilters);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('apply')} </Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
onChange(null);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('cancel')}
</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView >
);
}
}
So when I press the cancel button, null is returned to the _onChangeFilter function of the list screen. This part works, and according to console.log and the debugger, the setState is not called. But if i set a breakpoint within the else part, i can see that this.state.usedFilters has changed.
Ok after a while i figured it out. The problem was that the whole filters list was always just referenced since react native (js) seems to always use references, even when changing sub-parts of the lists.
fixed that by using lodash cloneDeep.

How to pass data to the render method in react-native

I am trying to display something fetched via graphql in my react-native was-amplify mobile app. I can't figure out how to pass that fetched data to my render method. Here's the source code. I need to be able to show the contents of the singletour object inside render. React throws an error when I try to reference this.props.singletour inside the render method. Another thing I can't figure out is how to pass the parameter received by navigation inside render to the GetTournament graphql query. Ideally I want the id: inside GetTournament to contain navigation.getParam('itemId', 'NO-ID') and not the hardcoded id. Again, react throws an error when I access this parameter inside the async method call...ANY help would be greatly appreciated!!
class DetailsScreen extends React.Component {
async componentDidMount() {
try {
const graphqldata = await API.graphql(graphqlOperation(GetTournament, { id: "4e00bfe4-6348-47e7-9231-a8b2e722c990" }))
console.log('graphqldata:', graphqldata)
this.setState({ singletour: graphqldata.data.getTournament })
console.log('singletour:', this.state.singletour)
} catch (err) {
console.log('error: ', err)
}
}
render() {
/* 2. Get the param, provide a fallback value if not available */
const { navigation } = this.props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Button
title="Go to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<Button
title="Go back"
onPress={() => this.props.navigation.goBack()}
/>
</View>
);
}
}
I think I know what you are trying to do, and it can be achieved with a refactor of your code.
This is what I would do:
Capture your navigation parameters in the constructor of your component and save them to state.
Set up an initial value for singleTour in state. Set a value for loaded in state. The loaded value will allow us to determine whether the data has come back successfully or not.
Refactor your componentDidMount so that it uses the itemId that is now stored in the state.
Refactor your console.log that checks whether you have set the state, as that is not being performed correctly.
In the render pull the values from state and handle wether the data has been loaded or not. You may wish to show some loading screen or not want to handle it at all.
Here is the refactor:
class DetailsScreen extends React.Component {
constructor (props) {
super(props);
// capture the values that you have passed via your navigation in the constructor
const { navigation } = props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
this.state = {
itemId: itemId,
otherParam: otherParam,
loaded: false,
singletour: [] // you don't state what singletour is but you should set a default value here
};
}
async componentDidMount () {
try {
// we can now use the state value for itemId as we captured it in the constructor of the component
const graphqldata = await API.graphql(graphqlOperation(GetTournament, { id: this.state.itemId }));
console.log('graphqldata:', graphqldata);
// this is a bad way to access state after it has been set,
// state is asynchronous and takes time to set.
// You would need to access set by using the callback method
// this.setState({ singletour: graphqldata.data.getTournament });
// console.log('singletour:', this.state.singletour); // <- you should never do this after you setState
// this is how you should access state after you have set it
// this will guarantee that the state has been set before the
// console.log is called, so it should show the correct value of state
this.setState({
singletour: graphqldata.data.getTournament,
loaded: true // we are going to use the loaded value to handle our render
}, () => console.log('singletour:', this.state.singletour));
} catch (err) {
console.log('error: ', err);
// you may want to show an error message on the screen.
}
}
render () {
// access the passed parameters from state
const { itemId, otherParam, loaded, singletour } = this.state;
if (loaded) {
// if once the data is loaded we can show screen
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Text>singletour: {JSON.stringify(singletour)}</Text>
<Button
title="Go to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<Button
title="Go back"
onPress={() => this.props.navigation.goBack()}
/>
</View>
);
} else {
// while we are waiting for the data to load we could show a placeholder screen
// or we could show null. The choice is yours.
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Data not loaded</Text>
</View>
);
}
}
}
Note that componentDidMount get called after the first render occurs, this is why we have the loaded value in state. By using loaded it allows us to handle what is presented to the user rather than presenting a screen where the data hasn't finished loading.
This is clearly one possible refactor of your code. There are many other ways that it could be refactored.
Here are some great articles on setting state
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6