React does not rerender when a prop changes - reactjs

I'm trying to develop an edit mode for a application.
In edit mode some buttons should have a lower opacity.
A boolean variable stores if the edit mode is active. This variable is passed down to its childs using props. If I now change the editMode in the parents state, the childs are not being rerendered.
Parentclass:
export default class Parentclass extends Component{
constructor(props){
super(props);
this.state = {
editMode: false,
};
}
render(){
return(
<View>
<EditButtonClass onEditPress={() => this.editButtonPress()}/>
<View>
<Subclass editMode={this.state.editMode}/>
</View>
</View>
);
}
editButtonPress(){
this.setState({editMode: true})
}
}
Subclass:
export default class Subclass extends Component{
render(){
return(
<View>
<Finalsubclass editMode={this.props.editMode}/>
</View>
);
}
}
Final subclass:
export default class Finalsubclass extends Component{
createStyle(){
return{
opacity: this.props.editMode ? 0.5 : 1,
}
}
render(){
return(
<TouchableOpacity style={this.createStyle()}/>
);
}
}
The button:
<TouchableOpacity onPress={() => this.props.onEditPress()}>
<Image source={require('../res/edit-button.png')} style=styles.editButton}/>
</TouchableOpacity>
The editMode in props does change. If I click on one of the buttons they're getting brighter. But not directly if I enable editmode.
Whats the best way to achieve a full rerendering?

you can user react lifecycle to re-render component
https://reactjs.org/docs/react-component.html
and for above issue you can use
componentWillReceiveProps(nextProps) {
...
}

The solution was to build a View around the TouchableOpacity and applying the styles to the view.

As componentWillReceiveProps is deprecated, i would suggest using componentDidUpdate.
To answer the question in your comment, you need to check the prevProps with the new ones to not get an infinite loop.
For example:
componentDidUpdate = (prevProps) => {
if(prevProps!==this.props){ /if the previous props are different from the current props
//do what you need
}
}
As it is an object, if you need to only check a single variable you can simply do:
if (prevProps.foo !== this.props.foo) {
//do what you need
}

Related

React Native setState not re-rendering

Expected behaviour of this component is like this: I press it, selectedOpacity() function is called, state is updated so it now renders with opacity=1.
But for some reason, after calling this.setState, it is not being re-rendered. I have to click this component again to make it re-render and apply changes of opacity from state.
export default class Category extends Component {
state = {
opacity: 0.5
}
selectedOpacity() {
// some stuff
this.setState({opacity: 1})
}
render() {
return(
<TouchableOpacity style={[styles.container, {opacity: this.state.opacity}]} onPress={() => {
this.selectedOpacity();
}}>
</TouchableOpacity>
)
}
I think what you are missing is binding of selectedOpacity(), else this would be undefined in it AFAIK.
Also better move the assignment of state to a constructor().
constructor(props) {
super(props);
this.state = {};
this.selectedOpacity = this.selectedOpacity.bind(this);
}
Also change to the following because creating an arrow function inside render affects performance.
onPress={this.selectedOpacity}
Change selectedOpacity to arrow function:
selectedOpacity = () => {
this.setState({opacity: 1})
}
Then:
onPress={this.selectedOpacity}
Edit: The react documentation says its experimental and the syntax is called public class field syntax.
Try change onpress to
onPress={() => this.selectedOpacity()}

how to get the updated data from parent to child as props

currently, I m trying to pass props data from parent to child and it works fine, but when I m also extracting a field from asyncStorage in the constructor (let's call it brokerName) and then storing it in the props. This is where the issue arrives, the props I m getting in the child element is without brokerName.
This is the parent:
constructor(props) {
super(props);
this.getBrokername();
}
getBrokername = async () => {
const brokerName = await AsyncStorage.getItem('brokerName');
this.props = { brokerName };
console.log('brokerName', brokerName);
}
render () {
return (
<View style={{flex: 1}}>
<View style={{ flex: 1 }}>
<VehicleDetails parentProps={this.props} />
</View>
</View>
);
}
This is the child:
export default class VehicleDetails extends React.Component {
constructor(props) {
super(props);
console.log('vehicleDetails', this.props); // I m not able to get this.props.brokerName
}
}
Any kind of help would be really appreciated.
Thanks.
A few things here.
You should never mutate your props, props are read only, what you want to use in those kinds of situations is state you should read this docs section
Async actions are side effects. At this moment (react 16) you should not have any side effects in the class constructor or render method.
What you're doing doesn't work because your code is async, that means that when the component is created you dispatch a request to fetch some data, but, by the time your component renders that data is not ready to display, another problem originates from my first point, as you're mutating the props instead of using state react doesn't know that it needs to re-render and that's the root of your problem.
To fix this:
Move your async request to componentDidMount lifecycle method check the lifecycle methods here
set state when the request is data is ready
Inject the state as a prop in your child component
Parent
constructor(props) {
super(props);
this.state={
brokerName:null
}
this.getBrokername();
}
getBrokername = async () => {
const brokerName = await AsyncStorage.getItem('brokerName');
this.setState({brokerName:brokerName})
console.log('brokerName', brokerName);
}
render () {
return (
<View style={{flex: 1}}>
<View style={{ flex: 1 }}>
<VehicleDetails brokerName={this.state.brokerName}
/>
</View>
</View>
);
}
Child
export default class VehicleDetails extends React.Component {
constructor(props) {
super(props);
console.log('vehicleDetails', this.props);
}
}
you can get parents props in props key ex : <parent name={name}/> so u can access name using this.props.name
after reading Diogo Cunha's answer, Asif vora's example and Akash Salunkhe's comment above, I came up with this solution and its working fine.
Thanks for all your help.
constructor(props) {
super(props);
this.state={
brokerName:null
}
console.log('details props', this.props);
}
componentDidMount() {
this.getBrokername();
}
getBrokername = async () => {
const brokerName = await AsyncStorage.getItem('brokerName');
this.setState({brokerName:brokerName});
console.log('brokerName', this.state, brokerName);
}
render () {
return (
<View style={{flex: 1}}>
<View style={{ flex: 1 }}>
{this.state.brokerName ? <VehicleDetails parentProps={this.props} brokerName={this.state.brokerName} /> : null }
</View>
</View>
);
}
Please feel free to give your suggestion for any kind of improvement in the answer.

React Native Make State Change From different component

I am using react native and I have a situation where I navigate to a new component using react navigation v2 stack navigator. The user then presses an option and goes back to the original screen they were at with updated information.
My question is how do change the state of the previous screen so it shows the information the user selected?
ShowFruitPage.js
This page shows a list of fruits that the user picked.
export default class ShowFruitPage extends Component{
constructor(){
super();
this.state = {
List: [{Fruit: apple}]
}
}
render(){
return(
<View style={styles.ViewStyle}>
<FlatList
data={this.state.List}
renderItem={({item}) =>
<TouchableNativeFeedback
background={TouchableNativeFeedback.Ripple('grey')}
onPress={() => this.props.navigation.navigate('AddFruit',
{
List: this.state.List
})}
<View style={styles.ListView}>
<Text>{item.Fruit}</Text>
</View>
</TouchableNativeFeedback>
}
/>
</View>
)
}
}
AddFruit.js
This page shows a list of available fruits the user can pick.
When the user picks from this list I want to update the list on the ShowFruitPage.
export default class AddFruit extends Component{
constructor(){
super();
this.state = {
FruitList: [{Fruit:orange}, {Fruit: pear}]
}
this.pickFruit = this.pickFruit.bind(this);
}
pickFruit(Fruit){
//Add the picked fruit to the ShowFruitPage state List
//Then Navigate back to ShowFruitPage
this.props.navigation.navigate('ShowFruitPage')
}
render(){
return(
<View style={styles.ViewStyle}>
<FlatList
data={this.state.FruitList}
renderItem={({item}) =>
<TouchableNativeFeedback
background={TouchableNativeFeedback.Ripple('grey')}
onPress={() => this.pickFruit(item)}
<View style={styles.ListView}>
<Text>{item.Fruit}</Text>
</View>
</TouchableNativeFeedback>
}
/>
</View>
)
}
}
Just as you did in ShowFruitPage.js with List, you could navigate back with some state, i.e.
this.props.navigation.navigate('ShowFruitPage', { Fruit });
to make { Fruit } available on navigation.state.params when you navigate back to ShowFruitPage.
Another possibility (I think) is to provide a callback when you navigate to AddFruit that can set the data on the ShowFruitPage allowing you to then just call navigation.goBack():
setFruit = fruit => {
this.setState({ FruitList: [...this.state.FruitList, fruit] });
}
this.props.navigation.navigate('AddFruit', { setFruit: this.setFruit });
And then:
pickFruit(Fruit){
const { navigation } = this.props;
navigation.state.params.setFruit(Fruit);
navigation.goBack();
}
An alternative and heavier solution, would be to implement some state management such as redux so that the data becomes independent of the individual page components.

Where do I call setState for redux values?

I'm pretty new to react native and async programming, and trying to understand how to "sync" redux state values and local state values.
For example, I have a text field "aboutMe" stored server side, and using mapStateToProps to place it into props:
const mapStateToProps = (state) => {
return { aboutMe: state.aboutMe };
}
In render, I have a TextInput I'm using so that the user can edit this field, and I would like to default to what is saved on the server side:
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
Basically, somewhere I need to call
this.setState({ aboutMe: this.props.aboutMe });
Where is the right place to this? I was trying to use componentWillReceiveProps, but that lifecycle method is not called on constructor, so I would need to setState twice (in constructor and in componentWillReceiveProps).
Is there another way to do this? I feel like this is a pretty generic problem that a lot of react native developers have solved but I couldn't find a generally accepted way online.
Thanks!
Edit:
I have alot of TextInputs, so I have a separate button to call the action to save the variables:
<Button onPress={()=>{
this.props.saveUserInput(this.state.aboutMe,
this.state.name, this.state.address, ....}}>
<Text> Save changes </Text>
</Button>
From the comments, I understand that it's possible to call the save action onChangeText... but is that too much traffic back and forth? Would it be better to save all of the variables locally to state and then call a save for everything at once? Also, what if the user would like to "cancel" instead of save? The changes would have been already saved and we will not be able to discard changes?
1) If your component is a controlled component (you need state in it) and the request is asynchronous indeed you have to set the state in the componentWillReceiveProps like this:
class ExampleComp extends Component {
constructor(props) {
super(props);
this.state = {
aboutMe: ""
}
}
componentWillReceiveProps(nextProps) {
this.setState({
aboutMe: nextProps.aboutMe,
});
}
render() {
return (
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
);
}
}
Keep in mind the key here is that the state must remain the single source of truth from now on.
2) The other option would be, you can wait until the request is finished in the parent component and then set the aboutMe in your constructor, this way you can avoid componentWillReceiveProps. For example:
class ParentComp extends Component {
render() {
return (
<div>
{this.props.aboutMe && <ExampleComp/>}
</div>
);
}
}
class ExampleComp extends Component {
constructor(props) {
super(props);
this.state = {
aboutMe: props.aboutMe
}
}
render() {
return (
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
);
}
}
The downside of this is that the text input won't be shown until the request is finished.
Since you have edited your question, it is more clear what you want to achieve, so I want to address that.
You could keep the state of your controlled input elements in the component, then use the redux store to store persistent data and to populate the default values.
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
aboutMe: props.aboutMe,
... // other data
}
}
handleSubmit = (event) => {
event.preventDefault() // To prevent redirect
// Dispatch the save user input action
this.props.dispatch(saveUserInput(this.state))
}
render() {
return (
<form onSubmit={this.handleSubmit} />
<TextInput onTextChange={text => this.setState({...this.state, aboutMe: text}) />
... // input fields for other data
// Clicking this fill trigger the submit event for the form
<button type="submit">Save</button>
</form>
)
}
}

Appended React Native component not rerendering

I'm attempting to write a function to remove a React Native component (named "Card") from the DOM on-click, then append a new "Card" of the same class with different properties. For example, both Cards have background colors. If the first Card is green, the second Card, which should have a blue background, will inherit the green background of the original Card.
The Cards receive their background color passed as props, like so:
class Card extends Component {
constructor(props) {
super(props);
this.state = {
style: {
backgroundColor: this.props.card.backgroundColor
}
};
}
render() {
return (
<TouchableHighlight style={this.state.style}>
<Image source={this.props.card.img} />
</TouchableHighlight>
)
}
}
The main component looks like this:
class SetProject extends Component {
constructor(props) {
super(props);
this.state = {
cardArray: [{backgroundColor: 'green', img: require('~/SetProject/cardImages/ovals/1-red-empty-oval.png')}]
}
}
removeCard(){
let emptyArray = [];
this.setState({cardArray: emptyArray});
}
changeCard(){
// let emptyArray = [];
// this.setState({cardArray: emptyArray});
let newCardArray = [{backgroundColor: 'red', img: require('~/SetProject/cardImages/ovals/1-purple-shaded-oval.png')}]
this.setState({cardArray: newCardArray});
}
render() {
let cardElementArray = this.state.cardArray.map(theCard => {
return (
<Card card={theCard}></Card>
);
});
return (
<View>
<View>
{cardElementArray}
</View>
<TouchableHighlight>
<Text onPress={this.removeCard.bind(this)}>Remove Card</Text>
</TouchableHighlight>
<TouchableHighlight>
<Text onPress={this.changeCard.bind(this)}>Change Background</Text>
</TouchableHighlight>
</View>
);
}
}
So I've got two buttons: removeCard, which works great, and changeCard. If I press "Remove Card" and then press "Change Card," I see the exact results I'm looking for. The card is removed and is replaced by a new one. However, if I comment in these lines in changeCard:
// let emptyArray = [];
// this.setState({cardArray: emptyArray});
and press "Change Card" without pressing "Remove Card," the new Card has a new image but it keeps the background color of the previous Card. This also happens if I call this.removeCard() from changeCard.
In summary, I'd like to be able to perform the behavior of both of these functions simultaneously, but I'm only able to remove a Card and add a new, correctly rendered Card if I press both buttons separately.
Any ideas would be much appreciated!
Here you're using props for setting image but not setting style. You can use props as well. You have set the style in constructor. Then you want to change style but constructor is not called again but creating a new object.
You can use props setting styl as well
render() {
return (
<TouchableHighlight style={this.props.card.style}>
<Image source={this.props.card.img} />
</TouchableHighlight>
)
}
For better implementation in case properties of card gets more complex add an id property to card. You can use componentWillReceiveprops by this way unnecessary renders are neglected as well.
[{id:'1', backgroundColor: 'red', img:
require('~/SetProject/cardImages/ovals/1-purple-shaded-oval.png')}]
class Card extends Component {
constructor(props) {
super(props);
this.state = {
style: {
card: this.props.card
}
};
}
componentWillReceiveProps(nextProps){
if(nextProps.card.id != this.state.card.id)
{
setState({card:nextProps.card})
}
}
render() {
return (
<TouchableHighlight style={this.state.style}>
<Image source={this.props.card.img} />
</TouchableHighlight>
)
}
}
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops
Don't you get warning about missing keys in array? Use unique identifier (or it's index in array as last resort) for each card, and use it to set key prop on each item in array. This way, when card in array changes, react can re-render it, because it's a new card to it.
let cardElementArray = this.state.cardArray.map(theCard => {
return (
<Card key={theCard.id} card={theCard}></Card>
);
});
Read more about keys here in React docs.

Resources