How to re-render a react-native components when its prop changes? - reactjs

Sorry if this is a bit of a noob question. I'm trying to set up a react-native multiplayer game with a lobby which shows who your opponents are. The problem is that even when the prop representing the opponents updates, the component does not re-render, even though a console log shows me the prop has changed (and that render() was called?). Some snippets from my code are below:
In Lobby.js:
export default class Lobby extends Component {
render() {
console.log('In Lobby, opponents = ' + this.props.opponents);
return(
<View style={{ flex: 1, justifyContent: "center", alignItems: 'center' }}>
<Text>Welcome to the lobby { this.props.username }!</Text>
<Text>Opponents: { this.props.opponents }</Text>
... more text and buttons ...
</View>
);
}
}
As I said, I can see in the console that this.props.opponents does change but the screen doesn't seem to re-render that <Text> component. I initially thought this may be because this.props.opponents is set to the opponents value of my Main component's state, but the console log seems to debunk this.
I've looked here on SO and found suggestions to add shouldComponentUpdate() and componentDidUpdate() to my component but the nextProps parameter would always actually be the same as this.props. I.e. if I add:
shouldComponentUpdate(nextProps) {
console.log('this.props.opponents=' + this.props.opponents + '; ' + 'nextProps.opponents=' + nextProps.opponents);
return this.props.opponents != nextProps.opponents;
}
This never actually returns True for me, even though the prop is definitely changing. (I also don't know what code I would actually want in componentDidUpdate() either, since I just want it to re-render). It just shows that, yes the prop is different but that both nextProps.opponents and this.props.opponents have changed to that same new value (i.e. 'TestUser').
I'm really at a loss here. Thanks in advance.
EDIT: Added simplified version of my code.
App.js:
import React, { Component } from 'react';
import Main from './components/Main';
export default function App() {
return (
<Main/>
)
}
In Main.js:
import React, { Component } from 'react';
import { View } from 'react-native';
import Lobby from './Lobby';
export default class Main extends Component {
constructor(props) {
super(props);
this.state = {
opponents: [], // array of strings of other users' usernames
in_lobby: true // whether user is in lobby or not
};
this.testClientJoin = this.testClientJoin.bind(this);
}
testClientJoin() {
console.log('Called testClientJoin().');
let currentOpponents = this.state.opponents;
currentOpponents.push('TestUser');
this.setState({
opponents: currentOpponents
});
}
render() {
return(
<View style={{ flex: 1}}>
{
this.state.in_lobby &&
<Lobby
opponents={this.state.opponents}
testClientJoin={this.testClientJoin}
/>
}
</View>
);
}
}
In Lobby.js:
import React, { Component } from 'react';
import { View, Button, Text } from 'react-native';
export default class Lobby extends Component {
render() {
console.log('Opponents prop = ' + this.props.opponents);
return(
<View style={{ flex: 1, justifyContent: "center", alignItems: 'center' }}>
<Text>Welcome to the lobby!</Text>
<Text>Your opponents are {this.props.opponents}.</Text>
<Button
title='Add test opponent'
onPress={this.props.testClientJoin}
/>
</View>
);
}
}
So. When I tap the button to Add test opponent, the console logs
Called testClientJoin().
Opponents prop = TestUser
which shows the prop has indeed updated. However, the doesn't actually reflect this change, until I force some update (i.e. since I'm using expo, I just save the file again, and I finally see the text appear on my phone. I'm sure I'm just being an idiot but would love to know how to get the desired behavior of the text component updating.

Well I found an answer to my question which I'll leave here if anyone finds this. What I had to do was the following:
Main.js:
Instead of passing this.state.opponents directly as a prop to <Lobby>, pass a copy of this array instead.
render() {
<Lobby
testClientJoin={this.testClientJoin}
opponents={ [...this.state.opponents] }
/>
And this has the desired result. So the moral of the story is, don't try to directly pass an array state from a parent to a child component.

Related

Null is not an object (evaluating 'firebase.auth().currentUser.email') : FIREBASE REACT NATIVE (EXPO)

I am using firebase in my react native project in expo CLI. I am trying to get the user information in another file that has been logged in. But for some reason i am facing the error that has been shown in this screenshot.
I tried initialising the firebase object in componentWillMount() as well as in the constructor. Yet it is throwing the same error, However if i come back and restart the app. It works just fine.
Dashboard.js
import React, { Component } from 'react'
import { Text, View , StyleSheet } from 'react-native'
import * as firebase from 'firebase';
import firebaseConfig from '../config';
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
export class DashBoard extends Component {
constructor()
{
super();
}
render() {
return (
<View style={styles.container}>
<Text>Hello {firebase.auth().currentUser.email} </Text>
</View>
)
}
}
export default DashBoard;
const styles= StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: 'center'
}
})
When the application starts, Firebase may need to communicate with its servers to check if the user authentication is still valid. This happens asynchronously, and while this is going on, firebase.auth().currentUser will return null. So that's why your <Text>Hello {firebase.auth().currentUser.email} </Text> doesn't work: you're calling .email on null.
The solution is to check if the user is authenticated:
<Text>Hello {firebase.auth().currentUser ? firebase.auth().currentUser.email : "unknown user"} </Text>
You'll typically want to do this in your regular JavaScript code, instead of in the render() method as it makes the code a bit harder to read.
In fact, you'll likely want to push the user into the state of your object, and refresh it when the authentication state changes. To do this, you can attach a so-called auth state listener and call setState from its callback:
componentWillMount() {
firebase.auth().addAuthStateChangedListener((user) => {
this.setState({ user });
});
}
And then in your render method you'd do:
<Text>Hello {user ? user.email : "unknown user"} </Text>

Ref.Current is Undefined

TLDR - I need help figuring out how to change a subcomponent color using refs.
I'm trying to teach myself a little more about React refs by doing a simple example: comparing a background color change in subcomponents with both props and refs. I realize this is not a best practice in the wild, however, for a toy example, it seemed like a good isolated exercise.
import React from 'react';
import logo from './logo.svg';
import './App.css';
import SubComponent1 from './SubComponent1'
import SubComponent2 from './SubComponent2'
class App extends React.Component {
render() {
let myRef = React.createRef();
return (
<div className="App">
<header className="App-header">
<SubComponent1
message = "Passing via props"
color = "orange"
/>
<SubComponent2
message = "Passing via ref"
ref={myRef}
/>
{console.log("hi")}
{console.log(myRef)}
{console.log(myRef.current)}
{/*{myRef.current.style = { backgroundColor: 'green' }}*/}
</header>
</div>
);
}
}
export default App;
I would like to be able to call myRef.current.style = { backgroundColor: 'green' } (or something to that effect) in my App.js file, however, it seems like myRef.current is null when I try to call it.
When I console log, I get {current : null}, but upon expanding, the component data is there. I read this may be because myRef.current gets wiped after compomentDidMount, but I'm not really sure where to go from here.
If I wanted to go about completing this example, what would be the best way for me to do so? Ideally, I think I'd like to be able to call the line I have commented out or something like it.
Code - https://github.com/ericadohring/ReactRef
you have to define the ref variable in component, not under the render function.
component ... {
myRef = React.createRef();
...
render() {
...
...
}
}

React-navigation to re-render route when parent state changes

I have a page which receives data from a source in specific Intervals and sets them into a state, I show part of that data inside my page, and I have a button which opens another page(via react-navigation) and I want rest of data to be displayed in there, but the problem is, it won't refresh the second page when state of parent changes, below is simplified code:
import React from "react";
import { View, Text, Button } from "react-native";
import { createStackNavigator, createAppContainer, NavigationActions } from "react-navigation";
class HomeScreen extends React.Component {
constructor() {
super();
this.state = { number: 0 };
}
genNumber() {
number = Math.random();
this.setState({ number });
}
componentWillMount() {
setInterval(() => { this.genNumber() }, 1000);
}
render() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text>Home Screen</Text>
<Button title="Go" onPress={() => this.props.navigation.navigate('Test', { k: () => this.state.number })} />
</View>
);
}
}
class TestScreen extends React.Component {
constructor() {
super()
this.state = { t: 1 };
}
render() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text>Test {this.props.navigation.getParam('k')()}</Text>
</View>
);
}
}
const AppNavigator = createStackNavigator({
Home: {
screen: HomeScreen,
},
Test: {
screen: TestScreen
}
});
export default createAppContainer(AppNavigator);
you are using navigation params to pass data from parent class to child class and you want a reflection of state change in child class but it won't work in it.
For that use Component and pass data as props you immediate reflection of data.
React will not re-render a screen if a data passed to it through parameters changed, unfortunately this is how react navigation works.
To solve it, you can change the way your are passing data between screens.
instead of using navigation parameters use a global state(example redux).
in this case set the number variable to a state in redux,
then select that number in your Test screen and when number changes it will trigger a re-render.

React Native Props passed to Child View won't behave as expected

I'm new to RN, trying to get around it by trial and erroring a lot. I'm currently stuck with this :
I have one parent view which is like this :
import React, { Component } from 'react';
import { View, Image, Text, Button } from 'react-native';
class ParentView extends Component {
render() {
return(
<View style={{flex:1}}>
<View style={{flex:0.14, flexDirection:'row', alignItems:'center'}}>
<View style={{flex:1}}>
<Image
source = {require('./assets/image1.png')}
resizeMode= 'contain'
style={{flex:1, height:null, width:null}}
/>
</View>
<View style={{flex:3}}>
<Button title='dummytitle' onPress={() => this.props.navigation.navigate('Child', {
dbpath: 'db.category.subcategory',
})}
/>
</View>
etc...
This part works OK. In child view, I'm trying to import JSON data from a file like so :
import React, { Component } from 'react';
import { Text, View, TouchableOpacity, Image, StyleSheet } from 'react-native';
import AwesomeAlert from 'react-native-awesome-alerts';
import db from './db/quizDB';
class Quiz extends Component {
constructor(props) {
super(props);
this.qno = 0
this.score = 0
quiz = this.props.navigation.getParam('dbpath');
arrnew = Object.keys(quiz).map(function(k) {return quiz[k]});
this.state = {
question: arrnew[this.qno].question,
}
};
render() {
return(
<View style={{flex:1}}>
<View style={{flex:2}}>
<View style={styles.Question}>
<Text>{this.state.question}</Text>
</View>
etc..
{this.state.question} returns nothing, it's just empty. But if I hardcode quiz as quiz = db.category.subcategory, it does work, {this.state.question} displays the expected content.
What am I missing there ? It seems like props aren't processed as I'd like them to...
You need to either declare quiz and arrnew with let or var, or attach them to the state.
In addition, in React, standard practice is not to directly attach properties to the class instance, like you've done here:
this.qno = 0
this.score = 0
These should probably be local variables, but if you need to, these could be attached to the state instead.
Solved this, I was being stupid. I just imported db in the Parent view and set the quiz prop directly from there. My mistake was to set it as a string...

Passing React Navigation to Child of Child Component

I'm dynamically building my "screen" with the use of child "row" and "button" components. I'm only using this method because I can't find a flex-flow property available for react-native.
So basically I'm mapping through an array of arrays to build each row, and within the row, mapping through each array to build each button. Because the onPress needs to be set in the button, I'm passing the URL for each
onPress{() => this.props.navigation.navigate({navigationURL})
as a prop, first to the row, and then to the button. The problem is I keep getting the error 'Cannot read property 'navigation' of undefined. I'm sure this is because only the actual "screens" within the navigator have access to the navigation props. I've also tried passing
navigation={this.props.navigation}
but had no success. I've looked through all of the documentation and can't seem to find anything helpful. Anyone else encountered a similar situation?
If you want to access the navigation object from a component which is not part of navigator, then wrap that component in withNavigation HOC. Within the wrapped component you can access navigation using this.props.navigation. Take a look at the official document
Sample
import { withNavigation } from 'react-navigation';
...
class CustomButton extends React.Component {
render() {
return <Button title="Back" onPress={() => {
this.props.navigation.goBack() }} />;
}
}
export default withNavigation(CustomButton);
Hope this will help!
Ahhh, silly mistake. I wasn't setting up Props in the constructor. Thank you Prasun Pal for the help! Here's my code if someone else has an issue.
import React, { Component } from 'react'
import { Image, Text, TouchableOpacity, View } from 'react-native'
import { withNavigation } from 'react-navigation'
class ButtonName extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<TouchableOpacity
onPress={() => this.props.navigation.navigate('PageName')}
>
</TouchableOpacity>
)
}
}
export default withNavigation(ButtonName);

Resources