ListView does not rerender after state change in react-native? - react-native

I am working on <ListView> component, I have a list of names but after changing the state, it's changing the values, but it's not re-rendering the ListView.
isSelect:false - (State)
isSelect:true - (State)
Code:
var designerName = [{id: 'Calvin Klein', name: 'Calvin Klein', isSelected: false},
{id: 'Donatella Versace', name: 'Donatella Versace', isSelected: false},
{id: 'Valentino Garavani', name: 'Valentino Garavani', isSelected: false},
{id: 'Giorgio Armani', name: 'Giorgio Armani', isSelected: false}];
export default class filter extends Component {
constructor() {
super();
const listDs = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
this.state = {
listDs:designerName
};
}
componentDidMount(){
this.setState({
designerDataSource:this.state.designerDataSource.cloneWithRows(this.state.listDs),
})
}
render() {
return (
<View>
<ListView
dataSource={this.state.designerDataSource}
renderRow={this.renderRow.bind(this)}
/>
</View>
);
}
renderRow(item){
return (
<TouchableHighlight key={item.id} onPress={this.onItemDesigner.bind(this, item)}>
<View>
<View>
{this.renderName(item)}
</View>
<View>
<Text>{item.name}</Text>
</View>
</View>
</TouchableHighlight>
);
}
renderName(item){
if(item.isSelected) {
return(
<Image
style={{
width: 15,
height:15}}
source={require('../images/black_tick_mark.png')} />
);
}
}
onItemDesigner(item){
var tempDesigner = this.state.listDs.slice();
for(var i=0; i<tempDesigner.length; i++){
if (tempDesigner[i].id == item.id && !tempDesigner[i].isSelected) {
tempDesigner[i].isSelected = true;
}else if (tempDesigner[i].id == item.id && tempDesigner[i].isSelected){
tempDesigner[i].isSelected = false;
}
}
this.setState({
designerDataSource: this.state.designerDataSource.cloneWithRows(tempDesigner),
});
}
}
Please kindly go through my above code and let me know, if you find any solution.
Thanks

The issue is part Javascript, part ListView. It stems from this function when you construct the ListView.DataSource.
const listDs = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
The rowHasChanged is what controls if a render should happen, and it's checking to see if the old object equals the new object.
Comparing two objects together doesn't return false if one of the items has changed. It's checking to see if the actual object itself is pointing to a different memory location.
Consider these examples.
let array1 = [{foo: 'bar'}]
let array2 = array1.slice()
// sliced array still contains references to the items
console.log(array2[0] === array1[0])
// => true
// change something in array1
array1[0].foo = 'test'
// you'll see the change in array2 as well
console.log(array2[0].foo)
// => 'test'
To make it have a new variable location, you can either create an empty {} object and iterate through the original keys and save the key/values to the new one, or, a little easier is to use JSON.parse(JSON.stringify(obj)) to clone the object.
array2[0] = JSON.parse(JSON.stringify(array1[0]))
console.log(array2[0] === array1[0])
// => false
You could technically clone the whole array that way, or only the one that changed. I would imagine it would be more efficient to only do it on the changed object to prevent unneeded renders.
Check out Object equality in JavaScript.

Related

I can’t modify value of a TextInput in a Flatlist

I’m currently trying to make my own board with some data that I fetch in my componentWillMount(), then, I store this data in a state. Until here, all is normal, I can display my data with some FlatList. But, in one of them, I use a TextInput to change a quantity value.
When the value is null, I can modify it without any problem. But, when there is a value (store with the fetch), I can’t modify it. When I try, the TextInput is replacing it by the default value. I don’t understand because in my onChangeText, I’m modifying the value in the array that I use.
I'm developping with React Native and Expo.
<FlatList
data={state.tableData}
keyExtractor={item => item.product_id + ""}
renderItem={({ item, index }) => (
<View style={[styles.columnRow, { backgroundColor: "#dff2ff" }]}>
<TextInput
style={styles.textInput}
maxLength={10}
textAlign={"center"}
keyboardType={"numeric"}
returnKeyType="next"
blurOnSubmit={false}
onChangeText={text => {
let { tableData } = this.state;
let newQte = "";
let numbers = "0123456789";
for (var i = 0; i < text.length; i++) {
if (numbers.indexOf(text[i]) > -1) {
newQte = newQte + text[i];
} else {
alert("Veuillez saisir uniquement des chiffres.");
}
}
tableData[index].quantity = newQte;
this.setState({
tableData
});
}}
value={item.quantity}
/>
</View>
)}
scrollEnabled={false}
/>;
I need to be able to modify the current value and to store it after.
The problem is you're mutating the state object of an item in onChangeText in tableData[index].quantity = newQte.
However, FlatList implements a shallow comparison of state, which means the sub-state quantity you're modifying for an item don't always trigger a render. For some reason it worked with null or undefined, probably due to internal optimizations.
The solution is to completely create a new array without altering the initial state :
<FlatList
// ...
onChangeText={text => {
// this line creates a copy of tableData array
const copyTableData = [...this.state.tableData];
let newQte = '';
let numbers = '0123456789';
for (var i = 0; i < text.length; i++) {
if (numbers.indexOf(text[i]) > -1) {
newQte = newQte + text[i];
console.log('new qte', newQte);
} else {
alert('Veuillez saisir uniquement des chiffres.');
}
}
// we're creating a completely new item object...
const newItem = {
...copyTableData[index],
quantity: newQte,
};
// ... which replace the initial item in our copy
copyTableData.splice(index, 1, newItem);
// We're updating the state, with a fresh new array
// This way React "see" there was a change and re-render the item
this.setState({
tableData: copyTableData,
});
}}
>

Select single checkbox from listview in React-native

I want to select only one checkbox, not multiple.
If i select two checkboxes one by one the previously selected checkbox should be unselected.
In my below code i can select multiple checkboxes.
import React ,{Component} from "react";
import CircleCheckBox, {LABEL_POSITION} from "react-native-circle-checkbox";
class Select_Delivery_Option extends React.Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
});
this.state = {
check_data:[],
dataSource: ds.cloneWithRows([]),
checked:false,
isLoading:false,
};
}
//I had call The componentDidMount for json Data here and bind it in Data source;
render() {
return ();
}
_renderRow(rowData: string, sectionID: number, rowID: number) {
return (
<View style={{ flex:1,flexDirection:'column',backgroundColor:'#FFF'}}>
<View style={{ flex:1,flexDirection:'row',backgroundColor:'#FFF'}}>
<View style={{flexDirection:'column',margin:10}}>
{rowData.adbHomeAddress}
<CircleCheckBox
checked={rowData.checked}
onToggle={()=>this._onPressRow(rowID, rowData,rowData.checked)}
labelPosition={LABEL_POSITION.LEFT}
label={rowData.Address1 +" ,\n "+ rowData.Address2 +",\n"+rowData.ctiName+", "+rowData.staName+", "+rowData.ctrName+","+rowData.adbZip+"."}
innerColor="#C72128"
outerColor="#C72128"
styleLabel={{color:'#000',marginLeft:10}}
/>
</View>
</View>
</View>
);
}
_onPressRow = (rowID,rowData,checked) => {
const {check_data,filter} = this.state;
console.log('rowdata',rowData);
console.log('rowid',rowID);
console.log('checked',checked);
rowData.checked = !rowData.checked;
var dataClone = this.state.check_data;
dataClone[rowID] = rowData;
this.setState({check_data: dataClone });
}
}
Link to the CircleCheckBox component used: https://github.com/paramoshkinandrew/ReactNativeCircleCheckbox
I had the same requirement and wasted hours looking for solution. Eventually, I was able to resolve the problem on my own.
Posting my answer below, l have used hooks in the example, let me know if someone wants a class-based solution.
const checkboxComponent = () => {
const [checkboxValue, setCheckboxValue] = React.useState([
{ label: 'Customer', value: 'customer', checked: false },
{ label: 'Merchant', value: 'merchant', checked: false },
{ label: 'None', value: 'none', checked: false },
])
const checkboxHandler = (value, index) => {
const newValue = checkboxValue.map((checkbox, i) => {
if (i !== index)
return {
...checkbox,
checked: false,
}
if (i === index) {
const item = {
...checkbox,
checked: !checkbox.checked,
}
return item
}
return checkbox
})
setCheckboxValue(newValue)
}
return (
<View>
{checkboxValue.map((checkbox, i) => (
<View style={styles.checkboxContainer} key={i}>
<CheckBox
value={checkbox.checked}
onValueChange={(value) => checkboxHandler(value, i)}
/>
<Text style={styles.label}>{checkbox.label}</Text>
</View>
))}
</View>
)
}
export default checkboxComponent
I suggest you to use FlatList instead of ListView it's more advance and easy to use component.
For your issue please create a state checkedItem: -1 and directly assign id of your item you check last then just add a check to your CircleCheckBox item. something like below code.
<CircleCheckBox
checked={rowData.id === this.state.checkedItem}
onToggle={(rowID)=> this.setState({ checkedItem: rowID})}
labelPosition={LABEL_POSITION.LEFT}
label={rowData.Address1 +" ,\n "+ rowData.Address2 +",\n"+rowData.ctiName+", "+rowData.staName+", "+rowData.ctrName+","+rowData.adbZip+"."}
innerColor="#C72128"
outerColor="#C72128"
styleLabel={{color:'#000',marginLeft:10}}
/>
Let me know if any query.

How to Render Realm ListView with Sections Header

I'm using react native with realm db. The realm schema is as follows:
static schema = {
name: 'TodoItem',
primaryKey: 'id',
properties: {
id: {type: 'string'},
value: {type: 'string'},
Category: {type: 'string'},
completed: {type: 'bool', default: false},
createdTimestamp: {type: 'date'}
}
}
export const todoItemDS = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2, sectionHeaderHasChanged: (s1, s2) => s1 !== s2})
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(todoItemsResults),
}
The ListView tag is as follows:
<ListView
dataSource={dataSource}
renderRow={this.renderRow.bind(this)}
renderSectionHeader={this.renderSectionHeader.bind(this)}
/>
and renderSectionHeader:
renderSectionHeader(sectionData, category) {
return (
<Text>{category}</Text>
)
}
renderRow(item){
const {dataSource, deleteTodoItem} = this.props
return (
<View style={{ justifyContent: 'space-between',flexDirection: 'row'}}>
<CheckBox onPress={(e) => this.completed(item.id,item.value,e.target.checked)} style={{marginTop: 15 }}checked={item.completed} />
<Text onPress={(e) => this.goToPageTwo(item.id)} style={{ alignSelf: 'center',flex:10}} >{item.value}
</Text>
<Button iconLeft large transparent primary style={{ height: 30 , flex:1 }} onPress={() => deleteTodoItem(item)}>
<Icon name="trash-o" style={{ color: 'red' }} />
</Button>
</View>)
}
I fill todoItems datasource from this function:
export const getTodoItems = () => {
const todoItems = TodoItem.get().sorted('createdTimestamp', true);
return todoItems
}
However, the rows and sections are rendered with empty sections text and empty rows text as shown in the image.
What is missing in this code and how can I render sections and rows correctly?
I added a listener to realm code that fills the data source as follows:
export const getTodoItems = () => {
console.log('create db:', Realm.path)
const itemData = {}
const todoItems = TodoItem.get().sorted('createdTimestamp', true).filtered('completed=false');
todoItems.addListener((items, changes) => {
// Update UI in response to inserted objects
changes.insertions.forEach((index) => {
if(itemData[items[index].Category]) {
itemData[items[index].Category].push(items[index])
} else
itemData[items[index].Category] = []//;
});
// Update UI in response to modified objects
changes.modifications.forEach((index) => {
});
// Update UI in response to deleted objects
changes.deletions.forEach((index) => {
// Deleted objects cannot be accessed directly
// Support for accessing deleted objects coming soon...
});
});;
todoItems.forEach((item) => {
if(itemData[item.Category]) {
itemData[item.Category].push(item)
} else
itemData[item.Category] = []//;
})
return itemData //todoItems
}
However, I can't see added items. The added item only shows up after adding another item. Any ideas?
The rendered SectionHeader is showing integer is because you didn't construct the dataSource in the right format, see the documentation here: link.
You need to construct something like:
const todoItemsData = {
categoryOne: [itemOne, itemTwo],
categoryTwo: [itemThree, itemFour]
}
But right now what you have is just an array of objects [realmItemOne, realmItemTwo], and if you just pass in an array of objects to construct the dataSource that gonna consumed by the cloneWithRowsAndSections, the category param in renderSectionHeader will becomes integer index accroding to here, Object.keys(arrayOfObjects) will return [0, 1, 2, ...]
So you want to map your todoItemsResults and construct the something like this
const itemData = {}
todoItemsResults.forEach((item) => {
if(itemData[item.Category] {
itemData[item.Category].push(item)
} else
itemData[item.Category] = [];
}
});
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(itemData),
}
For the rendered row not showing any data issue, I think is due to the same problem, the item param you are calling within renderRow should be a data attribute for one of you todoItem object. You can try replace {item.value} within {item} to see what item is actually is in your setting. I believe this will be solved if you can construct the right data to feed your dataSource.
For your follow up comments regarding listerning to Realm update:
You can do something like this
class DummyPage extends Component {
constructor(props) {
super(props)
this.realmUpdated = this.realmUpdated.bind(this);
realm.objects('todoItem').addListener('change', this.realmUpdated);
}
componentWillUnmount() {
realm.objects('todoItem').removeListener('change', this.realmUpdated);
}
realmUpdated() {
this.forceUpdate();
}
render() {
//build your data source in here and render the sectionList
}
}

React Native implement JSON data on ListView

I'm having a problem with implementing API data in ListView. I fetched JSON using Axios.
export function fetchRateService() {
return function(dispatch) {
axios.get(RATE_URL)
.then(response => {
dispatch({
type: FETCH_RATE_SERVICE,
payload: response.data
});
})
.catch((error) => {
console.log(error);
})
}
}
Reducer. I added rates data into array
import {
FETCH_RATE_SERVICE
} from '../actions/types';
const INITIAL_STATE = {
base: '',
date: '',
rates: []
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_RATE_SERVICE:
return {
...state,
base: action.payload.base,
date: action.payload.date,
rates: [ ...state.rates, action.payload.rates ]
};
default:
return state;
}
};
This is the component
class ConturyList extends Component {
componentWillMount() {
this.props.fetchRateService();
this.createDataSource(this.props);
}
createDataSource({rates}) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(rates);
}
renderRow(rate) {
return <ListItem rate={rate} />
};
render() {
console.log(this.props);
const { CardSectionStyle, textStyle, containerStyle } = styles;
const { visible, closeModal } = this.props;
return (
<Modal
visible={visible}
transparent={false}
animationType="slide"
onRequestClose={() => {this.props.closeModal()}}
>
<ListView
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
</Modal>
);
}
}
const mapStateToProps = state => {
return {
rates: state.rateService.rates,
date: state.rateService.date,
base: state.rateService.base
};
}
export default connect(mapStateToProps, { fetchRateService } )(ConturyList);
The problem is I can see the props data using console.log(this.props);
enter image description here
I'm spending more than 3 days to figure out why this is not working. I tried using map() adding on
renderRow(rate) {
return rate.map((data) => {
return <ListItem rate={data} />
};
but it did not work. All the conutry code is in one object, Do I need to split the data by commas?.
Appreciate you helps. Thank you
UPDATE
So I'm trying to implement FlatList instead using ListView. The problem is on the JSON data. enter image description here. I want to implement key which are CountryCurrencyCode(AUD, JPN, etc..) to FlatList. Since rates is an object within an object, I added rates object into an array(reducer). But this.props.rates[0] can't be implemented on data property of FlatList. What kind of method can I try? I can't think of anything. I could print out key using map() when rates is object and then I can't implement it on the FlatList.
I would recommend switching over to the new FlatList component over ListView. FlatList just accepts an array of data to hydrate.
Initiate this.state.datasource as an empty array
constructor(props) {
super(props);
this.state = {
dataSource: [],
}
}
Fetch your data and hydrate this.state.dataSource from your Redux reducer/action
ComponentDidMount(){
this.props.fetchRateService();
var myData = this.props.rates[0];
this.setState({
dataSource:myData
)}
}
Now that your this.state.dataSource is set, we can populate FlatList
<FlatList
data={this.state.dataSource}
renderItem={({item})=>this.renderRow(item)}
/>
Flat List will throw a warning about a key extractor
Add this line below to the FlatList component. You will need to change 'item.key' to fit your own unique child. You can just keep it out for now for development.
keyExtractor={item => item.key}
You should see your data now! Keep in mind, you don't have to set the this.state.dataSource. Its just how I do it. You can plug 'this.props.rates' array directly into FlatList instead. Check out the FlatList docs for all the different things you can do with it. Hope this helps!

Create a search bar in a listview

I am creating a Search bar in a list view with a list of multiples types of food, this search bar can filter the results.
When I search a specific food and select this food, If I erase the search bar another food is selected.
You can see the problem in this gif:
https://giphy.com/gifs/3oKIPpagpRTfnuA9vW
How can I solve this problem?
Code:
class ListFood extends Component {
constructor() {
super();
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
ds: ds,
dataSource: ds.cloneWithRows(dataDemo),
rawData: dataDemo,
};
}
setSearchText(event) {
let searchText = event.nativeEvent.text;
this.setState({searchText});
data = dataDemo
let filteredData = this.filterNotes(searchText, data);
ds = this.state.ds;
this.setState({
dataSource: ds.cloneWithRows(filteredData),
rawData: data,
});
}
filterNotes(searchText, notes) {
let text = searchText.toLowerCase();
return notes.filter((n) => {
let note = n.toLowerCase();
return note.search(text) !== -1;
});
}
render() {
return (
<View>
<TextInput
value={this.state.searchText}
onChange={this.setSearchText.bind(this)}
/>
<ListView
dataSource={this.state.dataSource}
renderRow={(data) => <Row state={this.state.CheckBoxState} data={data} />}
/>
</View>
);
}
}
row:
class Row extends Component {
constructor(props) {
super(props);
this.state = { checked: false };
}
render() {
return (
<View´>
<CheckBox
onPress={() => this.setState({
checked: !this.state.checked
})}
checked={this.state.checked}
/>
<Text>
{ this.props.data }
</Text>
</View>
);
}
}
I have had same problem before where the row change is not correctly detected. I did a workaround by initializing the datasource every time I had new data.
So try something like
setSearchText(event) {
let searchText = event.nativeEvent.text;
this.setState({searchText});
data = dataDemo
let filteredData = this.filterNotes(searchText, data);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.setState({
dataSource: ds.cloneWithRows(filteredData),
rawData: data,
});
}