Scenario:
I have a Login.js that I show as a Modal from multiple screens wherever I have placed the check to see if a user is logged in or not. After the user successfully login I change a key called LoggedIn to 1 from 0 using AsyncStorage. Now when a user successfully logged in and the Modal closes I want to rerender the scree user is on.
As I have a background in iOS, so there we have viewDidAppear that runs every time there is a Modal or user opens another app and comes back to the screen, etc.
So, what would be the equivalent of that in React Native? When a Modal close it should check if the LoggedIn value is changed in AsyncStorage and I've already prepared the components to render accordingly to the value of LoggedIn value.
Code:
I have a screen Profile.js in which I'm checking:
AsyncStorage.getItem("LoggedIn").then((value) => {
if (value === "1") {
setNeedLogin(true)
} else {
setNeedLogin(false)
}
});
const [needLogin, setNeedLogin] = useState(false);
Based on the above state I'm rendering the view as:
{
!needLogin &&
<View>
<Text>{userName}</Text>
<Text>{userEmail}</Text>
<TouchableOpacity>
<Text>Logout</Text>
</TouchableOpacity>
</View>
}
{
needLogin &&
<View>
<Text>You are not logged in</Text>
<Text>Please login or create a new account to see more information.</Text>
<TouchableOpacity onPress={() => {
alert('I am showing login screen here which is a modal')
}}>
<Text>Login or Sign Up</Text>
</TouchableOpacity>
</View>
}
Now when the Login.js renders the Modal and use logs in upon successful login I change the value of LoggedIn to 1 and close the modal which shows the Profile.js screen but when it shows it the view doesn't rerender. So, how would I check and change the state every time the profile.js view appears?
An equivalent of viewDidAppear in react native would be componentDidUpdate.
Sounds like you are dealing with 2 different components in the app, a Login and a Modal component.
One way to go about that would be passing a callback method to the Modal component if you extend the reusability of the Modal component.
For example,
class Login extends React.Component {
onLoginDone = props => {
// do some other things, like authenticate with the props data
}
render() {
<View>
<Modal onClose={this.onLoginDone} />
</View>
}
}
class Modal extends React.Component {
constructor(props) {
this.state = {
isVisible: false
}
}
onClose = () => {
this.setState({ isVisible: !this.state.isVisible })
this.props.onClose()
}
render() {this.state.isVisible && <View />}
}
Once the user has logged in (when you have validated the login credentials), you can change the state variable needLogin to false, this will re-render the screen, provided the state is connected to the screen that you want to re-render
First of all, you have to be clear in the mounting and updating process in react-native.
A componennt will re-redner whenever.
Props of the parents get updated.
State of the component gets updated.
Now coming to your problem, your login component will not re-render until the above two conditions fulfilled, and as you are using AsyncStorage it is not reactive too.
So either you have to use some reactive storage like redux-persist or you have to use focus listeners, I am assuming that you are using react-navigation so this focus listener might be a good fit for you.
Whenever the focus will be changed this function will be a trigger so you don't need to take care of updating the component etc.
import * as React from 'react';
import { View } from 'react-native';
function ProfileScreen({ navigation }) {
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// The screen is focused
// Call any action
});
// Return the function to unsubscribe from the event so it gets removed on unmount
return unsubscribe;
}, [navigation]);
return <View />;
}
https://reactnavigation.org/docs/function-after-focusing-screen/
Note: this focus listener will not work with react-native provided modal then you have to use react-navigation modal
If you don't want to use any focus listener or redux-persist you can simply check while opening the modal.
useEffect(()=>{
if(modalState){
AsyncStorage.getItem("LoggedIn").then((value) => {
if (value === "1") {
setNeedLogin(true)
} else {
setNeedLogin(false)
}
});
}
}, [modalState])
Related
I am learning React Native and this is my first post at Stack Overflow.
I'm trying to re-render a button to make its disabled property change from false to true when onPress.
This button is being manually rendered insiderenderMessageImage from react-native-gifted-chat.
I am doing it through a boolean in this.state, however I see the state value updating on the logs but the button remains visually the same and it can be pressed again and again without actually changing its disabled property.
I have in my Chat.js class:
constructor(props) {
super(props);
this.state = {
isButtonDisabled: false
};
}
Then from Gifted Chat I call renderMessageImage to show a custom message design when a message has an image:
renderMessageImage = (props) => {
return (
<View>
<Button
title="Disable Button"
disabled={this.state.isButtonDisabled}
onPress={() => this.disableButton(props.currentMessage.id)}
/>
</View>)
}
This custom design is just a button that should call another method and then disable self:
disableButton = async (message_id) => {
console.log("Disabling message_id: " + message_id); //This shows the msg_id correctly where the button is
console.log(this.state.isButtonDisabled); //This shows false
this.setState(previousState => ({
isButtonDisabled: true
}));
console.log(this.state.isButtonDisabled); //This shows true (should update button disabled property)
return true;
}
For what I have tested so far, I can see that state value of isButtonDisabled is correctly changing from false to true, and as I have read, a change in the state should make the component re-render, but unfortunately it is not working that way.
More in-depth testing:
I then headed to GiftedChat.js sources from the react-native-gifted-chat to try debugging some of the code and see what is going on there.
What I found is that whenever I press my custom button, the componentDidUpdate of GiftedChat.js is being called twice:
componentDidUpdate(prevProps = {}) {
const { messages, text, inverted } = this.props;
console.log(this.props !== prevProps); //This is called twice per button click
if (this.props !== prevProps) {
//this.setMessages(messages || []); //I changed this line for the line below to directly update state
this.setState({ messages });
console.log(this.props !== prevProps); //This is called once per button click (first of the two calls)
}
if (inverted === false &&
messages &&
prevProps.messages &&
messages.length !== prevProps.messages.length) {
setTimeout(() => this.scrollToBottom(false), 200);
}
if (text !== prevProps.text) {
this.setTextFromProp(text);
}
}
Once all this checked, I see that the state is being updated and the GiftedChat.js component and messages are being updated once per button click, however my button in renderMessageImage is never re-rendering to properly show its new disabled value and actually become disabled.
I am totally clueless what else to test, so I would really appreciate some help on what am I doing wrong.
Thank you very much in advance.
Edit: renderMessageImage is called by GiftedChat on my Chat.js class render() section, then GiftedChat will call this.renderMessageImage whenever a message has an image (eg. this.state.messages[i].image != undefined). That part is working correctly, everything is being called as it should, just the re-render of the button status (disabled) is not updating:
render() {
return (
<View style={{ flex: 1 }}>
<GiftedChat
messages={this.state.messages}
onSend={this.sendMessage}
user={this.user}
placeholder={"Write a message..."}
renderMessageImage={this.renderMessageImage}
/>
<KeyboardAvoidingView behavior={'padding'} keyboardVerticalOffset={80}/>
</View>
);
}
I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.
You can see it here
The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.
function App(props) {
const [displayCard, setDisplayCard] = useState(null)
return (
<div className="App">
<SearchDisplay handleCardSelect={setDisplayCard}/>
<CardDisplay card={displayCard} />
</div>
);
}
function SearchDisplay({handleCardSelect}) {
const [cards, setCards] = useState([]);
useEffect(() => {
(async () => {
const cards = await testCardSearch();
setCards(cards);
})();
}, []);
async function handleSearch(searchTerm) {
const searchCards = await cardSearch({name: searchTerm});
setCards(searchCards)
};
return (
<StyledDiv>
<SearchBar
handleSubmit={handleSearch}
/>
<ScrollView
handleCardSelect={handleCardSelect}
cards={cards}
/>
</StyledDiv>
);
}
function ScrollView({cards, handleCardSelect}) {
const [selected, setSelected] = useState(null);
return (
<ViewContainer>
{cards.map((card, idx) =>
<li
key={idx}
style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
onClick={() => {
setSelected(idx);
handleCardSelect(card);
}}
>
<Card card={card} />
</li>
)}
</ViewContainer>
);
}
The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard. And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.
A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.
Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.
React.memo acts similar to a PureComponent and will shallowly compare ScrollView's old props to the new props and only trigger a re-render if they're unequal:
export default React.memo(ScrollView);
React.memo also has a second argument, which gives you control over the comparison:
function areEqual(prevProps, nextProps) {
// only update if a card was added or removed
return prevProps.cards.length === nextProps.cards.length;
}
export default React.memo(ScrollView, areEqual);
If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.
By default (stateless) components re-render under 3 conditions
It's props have changed
It's state has changed
It's parent re-renders
This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.
// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)
export default React.memo(ScrollView, areEqual);
However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.
Try to remove key={idx} and check if this fixes your issue.
So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.
What is the logic of your setDisplayCard()?
I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.
This person seemed to be having a similar problem,
React functional component using state
I have a MainComponent which has several buttons with different texts. Upon button click the first time for any of them, it'll create a new window component. There is a flag that prevents it from creating more Windows unless this flag changes for some reason (I didn't need to include it here). Assume the code can create multiple Window components and add it to the list and render it successfully, which it does but if the mode is changed, click the buttons again won't create more components.
I render this array of objects by creating an state array and state flag and add new Window component to that array everytime a new one is created and render that array.
My problem is that when new Window create flag is off (flagObj !== null), I click any of the buttons which pass in differing values to the current windows, and I want the values to update. But the values DO NOT update. Each window.body prop accesses this.state.currentWindowData which is what I'm changing everytime a button gets clicked in this mode, and using dev tool I see that the state gets changed accordingly.
For some reason, it is not propagating down to the children Window components in the list. What is the problem?
MainComponent
constructor(props) {
super(props)
this.state = {renderWindow: false, windowArray: []}
this.manage= this.manage.bind(this)
}
openWindow() {
const newWindow = <Window key={shortid.generate()} body={this.state.currentWindowData}/>;
this.setState({
renderWindow: true,
windowArray: [newWindow, ...this.state.windowArray]
})
}
modify(val) {
this.setState({currentWindowData: val});
}
manageWindow(val) {
if (flagObj === null) {
this.setState({currentWindowData: val}, () => {
this.openWindow();
});
} else {
this.modifyWindow();
}
}
render() {
return (
<div>
<button onClick = {() => this.manage("text1")}/>
<button onClick = {() => this.manage("text2")}/>
<button onClick = {() => this.manage("text3")}/>
{this.state.renderWindow ? this.state.windowArray : null}
</div>
)
}
This is the window component. I render this.props.body (the state data passed in) and also do this.props.bodyData (set from componentWillReceiveProps), but neither update.
Window
constructor(props) {
super(props);
}
componentWillReceiveProps(nextProps) {
this.setState({bodyData: nextProps.body});
}
render() {
return (
<div>
{this.props.body}
{this.state.bodyData}
</div>
);
}
I'm trying to do Step 15 of this ReactJS tutorial: React.js Introduction For People Who Know Just Enough jQuery To Get By
The author recommends the following:
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
return (
<div className="alert alert-warning">
<strong>Oops! Too Long:</strong>
</div>
);
} else {
return "";
}
},
render() {
...
{ this.overflowAlert() }
...
}
I tried doing the following (which looks alright to me):
// initialized "warnText" inside "getInitialState"
overflowAlert: function() {
if (this.remainingCharacters() < 0) {
this.setState({ warnText: "Oops! Too Long:" });
} else {
this.setState({ warnText: "" });
}
},
render() {
...
{ this.overflowAlert() }
<div>{this.state.warnText}</div>
...
}
And I received the following error in the console in Chrome Dev Tools:
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be
a pure function of props and state; constructor side-effects are an
anti-pattern, but can be moved to componentWillMount.
Here's a JSbin demo. Why won't my solution work and what does this error mean?
Your solution does not work because it doesn't make sense logically. The error you receive may be a bit vague, so let me break it down. The first line states:
Cannot update during an existing state transition (such as within render or another component's constructor).
Whenever a React Component's state is updated, the component is rerendered to the DOM. In this case, there's an error because you are attempting to call overflowAlert inside render, which calls setState. That means you are attempting to update state in render which will in then call render and overflowAlert and update state and call render again, etc. leading to an infinite loop. The error is telling you that you are trying to update state as a consequence of updating state in the first place, leading to a loop. This is why this is not allowed.
Instead, take another approach and remember what you're trying to accomplish. Are you attempting to give a warning to the user when they input text? If that's the case, set overflowAlert as an event handler of an input. That way, state will be updated when an input event happens, and the component will be rerendered.
Make sure you are using proper expression. For example, using:
<View onPress={this.props.navigation.navigate('Page1')} />
is different with
<View onPress={ () => this.props.navigation.navigate('Page1')} />
or
<View onPress={ () => {
this.props.navigation.navigate('Page1')
}} />
The two last above are function expression, the first one is not. Make sure you are passing function object to function expression () => {}
Instead of doing any task related to component in render method do it after the update of component
In this case moving from Splash screen to another screen is done only after the componentDidMount method call.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Button,
Image,
} from 'react-native';
let timeoutid;
export default class Splash extends Component {
static navigationOptions = {
navbarHidden: true,
tabBarHidden: true,
};
constructor(props) {
super(props)
this.state = { navigatenow: false };
}
componentDidMount() {
timeoutid=setTimeout(() => {
this.setState({ navigatenow: true });
}, 5000);
}
componentWillUnmount(){
clearTimeout(timeoutid);
}
componentDidUpdate(){
const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}
}
render() {
//instead of writing this code in render write this code in
componenetDidUdpate method
/* const { navigate,goBack } = this.props.navigation;
if (this.state.navigatenow == true) {
navigate('Main');
}*/
return (
<Image style={{
flex: 1, width: null,
height: null,
resizeMode: 'cover'
}} source={require('./login.png')}>
</Image>
);
}
}
Call the component props at each time as new render activity. Warning occurred while overflow the single render.
instead of
<Item onPress = { props.navigation.toggleDrawer() } />
try like
<Item onPress = {() => props.navigation.toggleDrawer() } />
You can also define the function overflowAlert: function() as a variable like so and it will not be called immediately in render
overflowAlert = ()=>{//.....//}
I have recently implemented ReactTransitionsGroup to animate all my page transitions using react router (Velocty component is just a wrapper of ReactTransitionsGroup, so in practice is the same):
render () {
return (
<div className="app-layout">
<NavBar location={this.props.location} />
<VelocityTransitionGroup enter={{ animation: 'pageTransitionIn' }} leave={{ animation: 'pageTransitionOut' }}>
{React.cloneElement(this.props.children, {
key: this.props.location.pathname,
})}
</VelocityTransitionGroup>
<TimeOutModal />
<Notifications />
</div>
);
}
In my signup form, I have a a form component that once the signup request has successfully responded, the props will update and the component will trigger a redirect to the next signup step.
class SignUp extends Component {
componentDidUpdate () {
if (this.props.signup.success) {
hashHistory.push('/confirm-signup')
}
}
render() {//....//}
}
The problem is, once the redirect is triggered I enter in a infinite loop, since the ReactTransitionsGroup makes the component stay for a few miliseconds, triggering the redirect again.
I manage to "fix" it, using a setTimeout on the redirect, but I don't find this solution really elegant:
componentDidUpdate () {
if (this.props.signup.success) {
setTimeout(() => {
hashHistory.push('/confirm-signup')
}, 500)
}
}
Do you find any other solutions to this issue?
Thanks in advance!
Normally I would say to do the redirect in your form handler. Based on the fact that signup.success is a prop, I assume that you are using some sort of global store and you would have to rewrite a bunch of code to do that.
Edit
You could add a hasTransitioned property to your component, which you set when it transitions.
class Signup extends React.Component {
hasTransitioned = false
componentDidUpdate() {
if (this.props.signup.success && !this.hasTransitioned) {
this.hasTransitioned = true
hashHistory.push('/confirm-signup')
}
}
}