How to reset state when navigating away from page - react-native

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

Related

React Native: Back Button Closes Modal

I'm trying to make a text open a modal when the user pressed on it and closes when they press on the back button. The modal does not open when the user clicks on the text. I tried following the solution from other questions but it doesn't work and their modal are not in a separate component.
Login.js
const [modalOpen, setModalOpen] = useState(false);
return (
<CustomScrollView>
<ForgotPassword open={modalOpen}/> //Modal
<ForgotPasswordText onPress={() => setModalOpen(true)}>Forgot Password?</ForgotPasswordText>
</CustomScrollView>
);
ForgotPassword.js
const ForgotPassword = ({open}) => {
const [invisible, setInvisible] = useState({open});
return (
<Modal
statusBarTranslucent
transparent= {true}
visible={invisible}
animationType="fade"
onRequestClose={() => {setInvisible(!open);}}>
<CenteredModal>
<ModalView>
<ForgotPasswordTitle>Forgot Password</ForgotPasswordTitle>
</ModalView>
</CenteredModal>
</Modal>
);
}
Both components are mounted already. If you are changing the modalOpen state in your root component, it would not trigger the modal to be opened in the ForgotPassword component in the way it is implemented.
One solution would be to pass the modalOpen prop right away and using it in your Modal component for the visible prop. But you would need to pass your setModalOpen function from your Login component to remain the state, too.
const [modalOpen, setModalOpen] = useState(false);
return (
<CustomScrollView>
<ForgotPassword open={modalOpen} setOpen={setModalOpen} /> //Modal
<ForgotPasswordText onPress={() => setModalOpen(true)}>Forgot Password?</ForgotPasswordText>
</CustomScrollView>
);
const ForgotPassword = ({open, setOpen}) => (
<Modal
statusBarTranslucent
transparent={true}
visible={open}
animationType="fade"
onRequestClose={() => setOpen(!open)}>
<CenteredModal>
<ModalView>
<ForgotPasswordTitle>Forgot Password</ForgotPasswordTitle>
</ModalView>
</CenteredModal>
</Modal>
);
Find it working here:
https://codesandbox.io/s/broken-night-lu8e96

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

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

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.

The best way to create a scrollable tab in the middle of the screen?

The mobile app of Twitter has a scrollable tab in the middle of the screen when you are on your profile. The top half of the screen displaying your profile info etc doesn't change when you click on the scrollable tabs mid screen : "Tweets & replies", "Media" etc. I am wondering how one would create this? Having half the screen stay the same and then having tabs which change mid screen... At the moment I have react navigation tabs as my main navigation - so on one of these tabs (the profile tab) I want to create the same concept as the picture..
Late answer but (for anyone else and future reference), react-navigation uses this package, react-native-tab-view: https://github.com/react-native-community/react-native-tab-view
for their tabs.
You can nest this within a screen, just like you desire (the previous answer only addresses the navigator inside navigator and that isn't what you want).
Here is an example (not exactly like you want, but you get the idea that you can. so instead of a background image, swap it out and use a view or scrollview accordingly to create that layout):
https://snack.expo.io/#satya164/collapsible-header-with-tabview
cheers :)
EDIT: i just found a way with just using react-navigation after all:
https://snack.expo.io/#mattx/collapsible-header-tabs
check it out
and another library: https://github.com/benevbright/react-navigation-collapsible
I don't know if you've figured it out yet, but you can nest the TabNavigator inside a StackNavigator. That way, you can have a scrollable Tab.
class ProfileMenu extends React.Component{
render() {
return(
//whatever you wanted at the top
)
}
}
const TabNaviga = createMaterialTopTabNavigator({
Tweets: {screen: TweetScreen,},
Replies: {screen: RepliesScreen,},
})
const YNavigator = createStackNavigator ({
Home:{screen: TabNaviga,
navigationOptions: ({navigation}) => ({
header: <ProfileMenu navigation= {navigation} />,
})
}
})
export default YNavigator
I found this tutorial and followed it,
EDIT: it seems there's a new library out that supports it https://github.com/PedroBern/react-native-collapsible-tab-view
https://medium.com/#linjunghsuan/implementing-a-collapsible-header-with-react-native-tab-view-24f15a685e07
I also wrote a bit of an explaination if you are interested.
create a ScrollY with useRef and .current at the end
create a handleScroll function which returns an event like so -
const handleScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
);
Pass it down in props to the wanted component
<TabNavigator handleScroll={handleScroll} scrollY={scrollY} />
And also the scrollY so you can use the value in the Child component aswell
pass it farther down the line to actual events like and call handleScroll in the Child Child component onScroll prop. like so
<Animated.FlatList
...
onScroll={handleScroll}
/>
And now you can use the ScrollY value wherever you want.
what it does is checking if the current route is the one we check, it then caluclates the offset and scrollToOffset function of flatlist using the flatlist refs we got from here
return (
<Pictures
handleScroll={handleScroll}
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onGetRef={ref => {
if (ref) {
const found = listRefArr.current.find(e => e.key === route.key);
if (!found) {
listRefArr.current.push({ key: route.key, value: ref });
}
}
}}
/>
);
the onGetRef is connected to the FlatList ref
return (
<AnimatedFlatList
ref={onGetRef}
scrollToOverflowEnabled
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={handleScroll}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: HeaderHeight + TabBarHeight,
paddingHorizontal: 10,
minHeight: windowHeight - TabBarHeight
}}
data={data}
renderItem={({ item }) => {
return <Comment data={item} />;
}}
keyExtractor={({ commentId }): any => {
return commentId.toString();
}}
/>
);
then we have these three functions which we send the flatlist as well
const onMomentumScrollBegin = () => {
isListGliding.current = true;
};
const onMomentumScrollEnd = () => {
isListGliding.current = false;
syncScrollOffset();
};
const onScrollEndDrag = () => {
syncScrollOffset();
};
and last but not least we still need to animate the TabBar so when the header is 500 height his is 0 when the header is 450 in the y the tabbar should be 50, we do that by getting the scrollY in the props and use it to interpolate.
const renderTabBar = (props: any) => {
return (
<Animated.View
style={{
top: 0,
zIndex: 1,
position: "absolute",
transform: [{ translateY: tabViewHeight }],
width: "100%"
}}
>
<TabBar ... />
</Animated.View>
);
};

How can I Refresh Web View in React Native?

I have a webview as tab A and a todolist flatlist on tab B. If the user adds an entry to the flatlist on tab B, i want the tab A webview to refresh.
I couldn't find any .refresh() or reload() methods on the webview control https://facebook.github.io/react-native/docs/webview.html
Any ideas how to accomplish this?
You can set a key to the webview
key={this.state.key}
and then you can reload it by updating the state
this.setState({ key: this.state.key + 1 });
Well I reload WebView by doing following:
render() {
let WebViewRef;
return (
<View style={Style1.container}>
<WebView
ref={WEBVIEW_REF => (WebViewRef = WEBVIEW_REF)}
source={{ uri: this.state.site }}
renderLoading={this.ActivityIndicatorLoadingView}
startInLoadingState={true}
/>
<Button title="Reload Me!" onpress={() => { WebViewRef && WebViewRef.reload(); }} />
</View>
)
}
In this code I Declare Reference Variable WebViewRef first then assign this to WebView as ref={WEBVIEW_REF => (WebViewRef = WEBVIEW_REF)} and then call this reference for reload() as ()=>{ WebViewRef && WebViewRef.reload();}
The react-native-community/react-native-webview component has a .reload() method on the ref.
const webViewRef = useRef();
// ...
return (
<WebView ref={(ref) => webViewRef.current = ref} ... />
)
// ...
You can then use the following to reload:
webViewRef.current.reload();
I ended up using a dummy query parameter to signal a refresh of the web view:
In Tab B, I dispatch a change which changes "latestItemId" in the global state.
In Tab A, I use mapStateToProps which maps to <WebView source={{uri:URL?latestItemId=${latestItemId}}} /> in the render method. This causes it to think it's a new url and reload it.
Reload was not working on my end.
If you want to refresh on focus change you can use the hook useFocusEffect of react navigation and in the unmount clean the URL used in the webview. Then in the initialize you need to set that again. Maybe using a a useState.
useFocusEffect(
useCallback(() => {
setUrl(url!);
return () => {
setUrl(undefined);
};
}, [url]),
);
In my case I have source={{html}} so refresh() won't work in that situation. However, in my case I am still able to inject javascript so I can set some properties specifically document.styles.body.color to match dark and light mode.
const fg = colorScheme === "light" ? "black" : "white";
const webViewRef = createRef<WebView>();
useEffect(() => {
webViewRef.current?.injectJavaScript(`
document.body.style.color='${fg}';
true
`);
}, [fg]);
...
<WebView
ref={webViewRef}
originWhitelist={["*"]}
style={{
height: 200,
backgroundColor: "transparent",
}}
onMessage={() => {}}
javaScriptEnabled={true}
injectedJavaScript={`
document.body.style.color='${fg}';
true;`}
source={{ html }}
/>