how to finish modal animation in react-native-modal componentwillunmount? - react-native

I am looking for a solution to delay RN component unmount until react-native-modal animationOut is completed. This animation is triggered by setting isVisible=false.
With my current implementation the animation performs as expected on componentWillMount hook, however the problem is in componentWillUnmount the alert unmounts without an animation. I suspect this is because the component unmounts before the animation even has a change to start.
Here's the alert component:
//modal component
import Modal from "react-native-modal";
export default CustomAlert = props => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(true)
return () => setIsVisible(false);
}, [])
return (
<Modal
isVisible={isVisible}
animationIn={'slideInUp'}
animationOut={'slideOutDown'}
>
<View
...
</View>
</Modal>
)
};
Here's how the custom alert needs to be used:
export default ApplicationScreen = ({ navigation }) => {
const [alert, setAlert] = useState(null);
...
const doSomething = () => {
...
//show alert
setAlert({ title: 'title', message: 'message'});
...
//hide alert
setAlert(null);
...
}
...
return (
<SafeAreaView style={styles.container}>
{alert &&
<CustomAlert
title={alert?.title}
message={alert?.message}
...
/>
}
...
</SafeAreaView>
)
}

The problem is that you are containing the CustomAlert in a conditional rendering. Once the alert is set to false, the Modal would dismount directly.
You could pass the alert state into CustomAlert as property and then into Modal. The 's property isVisible would hanlde the render of the modal for you with animation.
ApplicationScreen:
<SafeAreaView style={styles.container}>
<CustomAlert
title={alert?.title}
message={alert?.message}
isAlertVisible={alert? true : false}
/>
</SafeAreaView>
in CustomAlert:
return (
<Modal
isVisible={props.isAlertVisible}
animationIn={'slideInUp'}
animationOut={'slideOutDown'}
>
<View
...
</View>
</Modal>
)

Related

React native component state not clearing/ umounting

I am using a the same component (BottomSheet) on two different screens in my react navigation stack.
When I go from one page to another, it keeps the component state. How can I refresh the component when I browse to another screen?
const HomeScreen = () => {
...
const showSheet = data.reminderVehicles.length !== 0 ? true : false;
return (
<BottomSheet
firstSnapshot={160}
secondSnapshot={90}
renderContent={renderSheetContent()}
tapToOpenEnabled
headerClose={true}
hide={showSheet}
>
.....
</BottomSheet>
);
};
export default HomeScreen;
export const MyVehicleScreen = ({ navigation }) => {
return (
...
<BottomSheet
firstSnapshot={160}
secondSnapshot={90}
renderContent={renderSheetContent()}
tapToOpenEnabled
hide={false}
>
</BottomSheet>
</RACPageStructure>
);
};
BottomSheet component:
const BottomSheet = ({
children,
firstSnapshot,
secondSnapshot,
renderHeader,
renderContent,
backgroundColor,
tapToOpenEnabled,
headerClose,
hide,
}) => {
...
return (
<WrapperComponent onPress={toggleOpen}>
<Animated.View style={childrenStyle}>{children}</Animated.View>
{!hide && (
<PanGestureHandler onGestureEvent={handleSwipe}>
....
</Animated.View>
</PanGestureHandler>
)}
</WrapperComponent>
);
};
Navigation stack:
return (
<TabStack.Navigator
initialRouteName={initialRouteName}
tabBarOptions={{
...
}}
>
<TabStack.Screen
name={AppRoutes.Home.Name}
...
</TabStack.Screen>
<TabStack.Screen
name={AppRoutes.MyVehicle.Name}
...
</TabStack.Screen>

React Navigation 5 headerRight button function called doesn't get updated states

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.
What changes need to be made to the navigation headerRight button to fix this issue?
Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.
import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
const submit = () => {
//label doesn't return the updated state here
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}
Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation,label]);
Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.
To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
const [saving, setSaving] = useState(false);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={() => setSaving(true)}>
<Text>Done</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
useEffect(() => {
// Check if saving to avoid calling submit on screen unmounting
if(saving){
submit()
}
}, [saving]);
const submit = () => {
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}

React Native - Rerunning the render method

I have a file here that defines an icon for the title.
static navigationOptions = ({ navigation }) => {
return {
headerRight: () => (<HomeHeaderIcon/>)
}
};
HomeHeaderIcon.js
export default class HomeHeaderIcon extends Component {
async componentDidMount(){
const token = await AsyncStorage.getItem('token');
this.setState({token});
}
state={
token:null
};
render() {
return (
<View>
{
this.state.token ===null
?
(
<TouchableOpacity
onPress={() => (NavigationService.navigate("LogStack"))}>
<Icon name="ios-power" size={30} style={{color: "white",marginRight:wp("5%")}}/>
</TouchableOpacity>
)
:
(
<TouchableOpacity
onPress={() => (NavigationService.navigate("Profile"))}>
<Icon name="ios-home" size={30} style={{color: "white",marginRight:wp("5%")}}/>
</TouchableOpacity>)
}
</View>
);
}
}
The system works exactly as I want. If there is a token, I say icon1 or icon2 show. The problem is I do this in componentDidMount, the icon does not change without refreshing the page. How do I render it again?
componentDidMount is called, as the name suggests, just once, when the component is mounted. Use componentDidUpdate to decide how your component behaves based on what piece of props or state has changed.
Read the documentation for more information regarding lifecycle methods.

Dismiss modal when navigating to another screen

I have an App with Home Screen, in this screen I'm rendering a Modal which opens on button press, inside the Modal I have a button that is supposed to navigate me to another screen, it's navigating correctly but when I navigate to another screen the modal doesn't disappear, how can i hide it?
Adding the code to demonstrate
Home:
import React, { Component } from 'react';
import Modal from './Modal';
class Home extends Component {
state = {
isModalVisible: false
};
toggleModal = () =>
this.setState({ isModalVisible: !this.state.isModalVisible });
render() {
const { navigate } = this.props.navigation;
<Modal
visible={this.state.isModalVisible}
navigation={this.props.navigation}
/>
);
}
}
export default Home;
Modal:
import React, { Component } from "react";
import Modal from "react-native-modal";
class Modal extends Component {
render() {
const { navigate } = this.props.navigation;
return (
<Modal
isVisible={this.props.visible}>
<Button onPress={() => {navigate('Main')}}>
>Button</Text>
</Button>
</Modal>
);
}
}
export default Modal;
Ideally you should wait for the setState to finish inside the callback and then navigate to the screen, since the methods are async and may disrupt the state if navigate is called before setState has finished completing.
Also parent should control the state of the child.
Home
onNavigate = () => {
this.setState({isModalVisible: false}, () => this.props.navigation.navigate('Main')
}
<Modal
visible={this.state.isModalVisible}
onNavigate={this.onNavigate}
/>
Modal
<Modal
isVisible={this.props.visible}>
<Button onPress={this.props.onNavigate}>
<Text>Button</Text>
</Button>
</Modal>
You should provide a reference to the variable that defines the visibility state of the modal component. You'll need to define a function hides the modal and pass the function reference to the modal component and execute it on press of the button along side with the navigation action.
Something on the lines of -
Your home screen should have a function like -
onModalClose = () => {this.setState({isModalVisible: false})}
then pass this as reference to the modal component like -
<Modal
visible={this.state.isModalVisible}
navigation={this.props.navigation}
onModalClose={this.onModalClose}
/>
and call it on the onPress() method of the <Button/> component like-
<Button onPress={() => {this.props.onModalClose(); navigate('Main')}}>
EDIT
Just noticed, since you already have a function that toggles the visibility of your modal, you need not define a new function. You can pass that function reference to the modal component itself.
<Modal
visible={this.state.isModalVisible}
navigation={this.props.navigation}
onModalClose={this.toggleModal}
/>
I took Pritish Vaidya answer and made it usable for any screen.
Home
import React, { Component } from 'react';
import Modal from './Modal';
class Home extends Component {
state = {
isModalVisible: false
};
toggleModal(screenName) {
this.setState({isModalVisible: !this.state.isModalVisible });
if (screenName && screenName != '') {
this.props.navigation.navigate(screenName);
}
}
render() {
<Modal
visible={this.state.isModalVisible}
onDismiss={(screenName) => { this.toggleModal(screenName); }}
/>
);
}
}
export default Home;
Modal:
class Modal extends Component {
dismissScreen(screenName) {
const dismissAction = this.props.onDismiss;
dismissAction(screenName);
}
render() {
return(
<View style={{ flex: 1, padding: 20 }}>
<Button
title="Dismiss Modal"
onPress={() => {this.dismissScreen();}}
/>
<Button
title="Navigate to Other Screen"
onPress={() => {this.dismissScreen('ScreenName');}}
/>
</View>
);
}
}

React Native, render function not rendering, although the trace indicates that passes through it

Solution added at the end of this question, inspired by https://github.com/facebook/react-native/issues/6268 from #grabbou
The curtain rises and the first scene 'Scene1' appears. Scene1 is a presentational component wrapped with 'connect' from react-redux framework to bind the status and actions to their props.
Actions work perfectly well and renders the state, the counter, on the screen.
Cliking forward to the second scene 'Scene2', exactly the same as the first component, but the props (the same as Scene1) are passed through passProps in renderScene within the Naviagator.
Every thing is OK, the actions are dispatched correctly, you can see on the trace, render function is invoked for painting the counter, again you can see in the trace, but DOES NOT WORK!. The inner component logs that is in the Scene1! What's wrong?
This is the trace, after going directly to Scene2 and click twice on <+> to increment the state.
It's a bug-Native React?
I am using
"react-native": "0.19.0",
"react-redux": "4.1.2",
"redux": "3.1.7",
This is all the code, if you can help me.
There are no concession to stylize the presentation, so the result on the screen is very simple.
1. The simple code of index.ios.js
'use strict';
import React, {
AppRegistry,
} from 'react-native';
import App from './src/App'
AppRegistry.registerComponent('App', () => App);
And 2. this is the code of the App.js:
'use strict';
import React, {
Navigator,
Component,
View, ListView, ScrollView,
Text, TouchableOpacity
} from 'react-native';
import { Provider, connect } from "react-redux";
import { createStore, applyMiddleware, combineReducers, bindActionCreators } from "redux";
import thunkMiddleware from "redux-thunk";
import createLogger from "redux-logger";
2.1 The redux part
// REDUX BEGIN
//Actions
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
//Actions creators
const increment = () => ({ type: INCREMENT })
const decrement = () => ({ type: DECREMENT })
//Redux Initial State
const initialState = {
counter: 0
}
//Reducer
const reducer = (state = initialState, action = {}) => {
let delta = 1
switch (action.type) {
case DECREMENT: delta = -1;
case INCREMENT:
return Object.assign({}, state, { counter: state.counter+delta })
default:
return state
}
}
//Redux Middelware
const loggerMiddleware = createLogger();
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore);
//Wrapper to bind state and actions to props on Presentational Component
const connectComponent = (component) => connect(
(state) => ({
counter: state.counter
}),
(dispatch) => ({
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement())
})
)(component)
// REDUX END
2.2 The App root, with the Provider and the Navigator
// APP
export default class App extends Component {
render () {
return (
<Provider store={createStoreWithMiddleware(reducer, initialState)}>
<Navigator style={{flex: 1}}
initialRoute={{
name: 'Scene1',
component: connectComponent(Scene1),
}}
renderScene={ (route, navigator) => {
const Component = route.component;
return (
<View style={{flex: 1, marginTop:40}}>
<Component navigator={navigator} route={route} {...route.passProps} />
</View>
);
}}
/>
</Provider>
)
}
}
2.3.
The inner Component in both scenes to render the counter.
Has some traces, to show that the shouldComponentUpdate is triggered and return True (you has to Update!) with the time traced to show that is invoqued just some milliseconds after an action is dispatched.
And other to show that the render function is reached, but doesn't not render with in the Scene2.
The trace show that this component always he thought that was the Scene1!!
class Counter extends Component{
constructor (props, context){
super(props, context);
}
shouldComponentUpdate(nextProps, nextState){
//Begin log
const repeat = (str, times) => (new Array(times + 1)).join(str);
const pad = (num, maxLength) => repeat(`0`, maxLength - num.toString().length) + num;
const formatTime = (time) => `# ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`;
console.log('shouldComponentUpdate '+this.props.route.name+ ': '+ (nextProps.counter !== this.props.counter) +' '+formatTime(new Date()));
//End log
return nextProps.counter !== this.props.counter;
}
render() {
console.log('onRender: '+this.props.counter);
return (
<View>
<Text style={{fontSize: 100}}>{this.props.counter}</Text>
<TouchableOpacity onPress={()=>{this.props.increment()}} ><Text style={{fontSize: 40}}>{'<'}+{'>'}</Text></TouchableOpacity>
<TouchableOpacity onPress={()=>{this.props.decrement()}} ><Text style={{fontSize: 40}}>{'<'}-{'>'}</Text></TouchableOpacity>
<Text>----</Text>
</View>
)
}
}
2.4.
The two scenes, are equals, just the button to forward or backward
class Scene1 extends Component {
render() {
return (
<View>
<Text style={{fontSize: 40}}>Scene1</Text>
<Counter {...this.props}/>
<TouchableOpacity onPress={()=>{
this.props.navigator.push({
name: 'Scene2',
component: Scene2,
passProps: {...this.props}
})
}}>
<Text style={{fontSize: 20}}>{'<'}Forward{'>'} to Scene2</Text>
</TouchableOpacity>
</View>
)
}
}
class Scene2 extends Component {
render() {
return (
<View>
<Text style={{fontSize: 40}}>Scene2</Text>
<Counter {...this.props}/>
<TouchableOpacity onPress={()=>{
this.props.navigator.pop()
}} >
<Text style={{fontSize: 20}}>{'<'}Back{'>'} to Scene1</Text>
</TouchableOpacity>
</View>
)
}
}
At the end some 'hard copy' to show the 'show'
The Scene2 showing the counter, and the two buttons to dispatch actions.
Clicking theses actions doesn't render the counter, but the actions are dispatched correctly.
After just going to Scene2 and two clicks on <+> to increment the counter.
The Counter component is his trace show the route.name, but it it show is on Scene1! What is wrong here?
Well, the play is over, the curtain has fallen.
It is a very dramatic scene. (Just the Scene2)
I wonder why it does not work.
Native React issue?
Thanks to all
The Solution
from https://github.com/facebook/react-native/issues/6268
#grabbou inspired the changes, he proposes wrap the all App as a Container and then pass Store and Actions as simple props to all Scenes.
To make these changes create a new component the RootComponent and render the App connected to the Redux Store and Actions like this.
export default class RootComponent extends Component {
render () {
const AppContainer = connectComponent(App); //<< App has to be container
return (
<Provider store={createStoreWithMiddleware(reducer, initialState)}>
<AppContainer/>
</Provider>
)
}
}
Then App change removing the Provider and just passing the Scene1 as dumb component, and renderScene pass {...this.props} insted of {...route.passProps}
class App extends Component {
render () {
return (
<Navigator style={{flex: 1}}
initialRoute={{
name: 'Scene1',
component: Scene1,
}}
renderScene={ (route, navigator) => {
const Component = route.component;
return (
<View style={{flex: 1, marginTop:40}}>
<Component navigator={navigator} route={route} {...this.props} />
</View>
);
}}
/>
)
}
}
The remove passProps from navigator.push in Scene1, because already are passed as default in renderScene
<TouchableOpacity onPress={()=>{
this.props.navigator.push({
name: 'Scene2',
component: Scene2,
//passProps: {...this.props}
})
}}>
And this is all folks!
Thanks
NOTE: This is merely a copy/paste of the author provided solution above.
from https://github.com/facebook/react-native/issues/6268
#grabbou inspired the changes, he proposes wrap the all App as a Container and then pass Store and Actions as simple props to all Scenes.
To make these changes create a new component the RootComponent and render the App connected to the Redux Store and Actions like this.
export default class RootComponent extends Component {
render () {
const AppContainer = connectComponent(App); //<< App has to be container
return (
<Provider store={createStoreWithMiddleware(reducer, initialState)}>
<AppContainer/>
</Provider>
)
}
}
Then App change removing the Provider and just passing the Scene1 as dumb component, and renderScene pass {...this.props} insted of {...route.passProps}
class App extends Component {
render () {
return (
<Navigator style={{flex: 1}}
initialRoute={{
name: 'Scene1',
component: Scene1,
}}
renderScene={ (route, navigator) => {
const Component = route.component;
return (
<View style={{flex: 1, marginTop:40}}>
<Component navigator={navigator} route={route} {...this.props} />
</View>
);
}}
/>
)
}
}
The remove passProps from navigator.push in Scene1, because already are passed as default in renderScene
<TouchableOpacity onPress={()=>{
this.props.navigator.push({
name: 'Scene2',
component: Scene2,
//passProps: {...this.props}
})
}}>
And this is all folks!
Thanks