In the following example:
MapView
displays elements of a ListView
as annotations
- Clicking on a
ListView
element should result in painting it in blue color.
- Bonus if the
MapView
and ListView
efficiently use the state object
Modifying the DataSource
of ListView
seems to cause the conflict when the active
attribute gets modified:
You attempted to set the key 'active' with the value 'false' on an
object that is meant to be immutable and has been frozen.
What is the right way of setting the state?
RNPlay Example
'use strict';
import React, {Component} from 'react';
import {AppRegistry,View,ListView,MapView,Text,TouchableOpacity} from 'react-native';
var annotations = [
{
title: 'A',active: false,latitude: 45,longitude: 26,latitudeDelta: 0.015,longitudeDelta: 0.015,
},{
title: 'B',active: false,latitude: 49,longitude: 14,latitudeDelta: 0.015,longitudeDelta: 0.015,
},{
title: 'C',active: false,latitude: 26,longitude: 25,latitudeDelta: 0.015,longitudeDelta: 0.015,
}
]
class SampleApp extends Component {
constructor(props) {
super(props);
var ds = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.state = {
region: annotations[0],
annotations: annotations,
dataSource: ds.cloneWithRows(annotations)
};
}
handleClick(field) {
if (this.previousField) {
this.previousField.active = false;
}
this.previousField = field;
field.active = true;
this.setState({
region: field,
});
}
renderField(field) {
let color = (field.active == true)?'blue':'yellow';
return (
<TouchableOpacity onPress={this.handleClick.bind(this,field)}>
<Text style={{backgroundColor:color,borderWidth:1}}>{field.title}</Text>
</TouchableOpacity>
);
}
render() {
return (
<View style={{flex:1,flexDirection:'column',alignSelf:'stretch'}}>
<MapView
style={{flex:0.5,alignSelf:'stretch',borderWidth:1}}
region={this.state.region}
annotations={this.state.annotations}
/>
<ListView
dataSource={this.state.dataSource}
renderRow={(field) => this.renderField(field)}
/>
</View>
);
}
}
AppRegistry.registerComponent('SampleApp', () => SampleApp);
The Problem
When you set field.active = true;
or this.previousField.active = false;
, you're modifying an object (field
) that is present in the datasource of your ListView
. The ListView
throws the error because it freezes its datasource when you create it using cloneWithRows
. This is to ensure that the datasource can't be modified outside of the normal React component lifecycle (like setState
). Instead, ListView.DataSource
objects are designed to be changed with cloneWithRows
, which returns a copy of the existing datasource.
If you're familiar with the Redux library, it's very similar to the philosophy of having reducer functions return a copy of the state, rather than modifying the existing state.
Cloning the DataSource
To solve this problem, instead of mutating field
objects in your handleClick
function, what you really want to do is create a new data array with values already set (like active
), and then call setState
with a new datasource for your ListView
created with cloneWithRows
. If you do this, you actually don't even need the annotations
key in your state at all.
Code is probably more helpful than words here:
handleClick(field) {
//iterate over annotations, and update them.
//I'm taking 'title' as a unique id property for each annotation,
//for the sake of the example.
const newAnnotations = annotations.map(a => {
//make a copy of the annotation. Otherwise you'll be modifying
//an object that's in your listView's datasource,
//and therefore frozen.
let copyA = {...a};
if (copyA.title === field.title) {
copyA.active = true;
} else {
copyA.active = false;
}
return copyA;
});
this.setState({
region: {...field, active: true},
dataSource: this.state.dataSource.cloneWithRows(newAnnotations),
});
}
I hope this helps! Here's a code snippet containing the complete code you posted, with my modifications. It's working for me just as you described it should on iOS using React Native 0.29. You tagged the question android-mapview, so I'm assuming you're running Android, but the platform shouldn't really make a difference in this case.
'use strict';
import React, {Component} from 'react';
import {AppRegistry,View,ListView,MapView,Text,TouchableOpacity} from 'react-native';
var annotations = [
{
title: 'A',active: false,latitude: 45,longitude: 26,latitudeDelta: 0.015,longitudeDelta: 0.015,
},{
title: 'B',active: false,latitude: 49,longitude: 14,latitudeDelta: 0.015,longitudeDelta: 0.015,
},{
title: 'C',active: false,latitude: 26,longitude: 25,latitudeDelta: 0.015,longitudeDelta: 0.015,
}
]
class SampleApp extends Component {
constructor(props) {
super(props);
var ds = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.state = {
region: annotations[0],
dataSource: ds.cloneWithRows(annotations)
};
}
handleClick(field) {
//iterate over annotations, and update them.
//I'm taking 'title' as a unique id property for each annotation,
//for the sake of the example.
const newAnnotations = annotations.map(a => {
//make a copy of the annotation. Otherwise you'll be modifying
//an object that's in your listView's datasource,
//and therefore frozen.
let copyA = {...a};
if (copyA.title === field.title) {
copyA.active = true;
} else {
copyA.active = false;
}
return copyA;
});
this.setState({
region: {...field, active: true},
dataSource: this.state.dataSource.cloneWithRows(newAnnotations),
});
}
renderField(field) {
console.log(field);
let color = (field.active == true)?'blue':'yellow';
return (
<TouchableOpacity onPress={this.handleClick.bind(this,field)}>
<Text style={{backgroundColor:color,borderWidth:1}}>{field.title}</Text>
</TouchableOpacity>
);
}
render() {
return (
<View style={{flex:1,flexDirection:'column',alignSelf:'stretch'}}>
<MapView
style={{flex:0.5,alignSelf:'stretch',borderWidth:1}}
region={this.state.region}
annotations={this.state.annotations}
/>
<ListView
dataSource={this.state.dataSource}
renderRow={(field) => this.renderField(field)}
/>
</View>
);
}
}
AppRegistry.registerComponent('SampleApp', () => SampleApp);