ListView is not re-rendering after dataSource has been updated - javascript

I am trying to implement a todo app in react-native with features like addTodo, removeTodo, markCompleted todos. After adding todos, when I press on markComplete text, the listView is not re-rendering, if I reload the app it displays expected results. I am using Firebase database to fetch my todos from.
Basically, I am updating a property in my listView datasource when I click on markComplete. Everything is working fine expect the re-rendering of listView whenever I press markComplete or Completed buttons on UI. I have tried a few solutions suggested in related question, I couldnt get it working.
To be more specific: please look at code below comment // When a todo is changed. I am updating my datasource in those lines of code when I changes something in items array.
Below is my code and snapshot of app UI.
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
ListView,
Text,
View,
TouchableHighlight,
TextInput
} from 'react-native';
var Firebase = require('firebase');
class todoApp extends Component{
constructor(props) {
super(props);
var myFirebaseRef = new Firebase('[![enter image description here][1]][1]database URL');
this.itemsRef = myFirebaseRef.child('items');
this.state = {
newTodo: '',
completed: false,
todoSource: new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2})
};
this.handleKey = null;
this.items = [];
} // End of Constructor
componentDidMount() {
// When a todo is added
this.itemsRef.on('child_added', (dataSnapshot) => {
this.items.push({
id: dataSnapshot.key(),
text: dataSnapshot.child("todo").val(),
completedTodo: dataSnapshot.child("completedTodo").val()
});
this.setState({
todoSource: this.state.todoSource.cloneWithRows(this.items)
});
});
// When a todo is removed
this.itemsRef.on('child_removed', (dataSnapshot) => {
this.items = this.items.filter((x) => x.id !== dataSnapshot.key());
this.setState({
todoSource: this.state.todoSource.cloneWithRows(this.items)
});
});
// When a todo is changed
this.itemsRef.on('child_changed', (dataSnapshot) => {
this.items.forEach(function (value) {
if(value["id"] == this.handleKey){
this.items["value"]["completedTodo"]= dataSnapshot.child("completedTodo").val()
}
});
this.setState({
todoSource: this.state.todoSource.cloneWithRows(this.items)
});
});
}
addTodo() {
if (this.state.newTodo !== '') {
this.itemsRef.push({
todo: this.state.newTodo,
completedTodo: this.state.completed,
});
this.setState({
newTodo : ''
});
}
console.log(this.items);
}
removeTodo(rowData) {
this.itemsRef.child(rowData.id).remove();
}
handleCompleted(rowData){
this.handleKey = rowData.id;
if(rowData.completedTodo){
this.itemsRef.child(rowData.id).update({
completedTodo: false
})
}
if(rowData.completedTodo == false){
this.itemsRef.child(rowData.id).update({
completedTodo: true
})
}
}
renderRow(rowData) {
return (
<View>
<View style={styles.row}>
<TouchableHighlight
underlayColor='#dddddd'
onPress={() => this.removeTodo(rowData)}>
<Text style={styles.todoText}>{rowData.text}</Text>
</TouchableHighlight>
<TouchableHighlight underlayColor='#dddddd' onPress={() => this.handleCompleted(rowData)}>
{rowData.completedTodo? <Text style={styles.todoText}>Completed</Text>:<Text style={styles.todoText}>MarkCompleted</Text>}
</TouchableHighlight>
</View>
<View style={styles.separator} />
</View>
);
}
render() {
return (
<View style={styles.appContainer}>
<View style={styles.titleView}>
<Text style={styles.titleText}>
My Todos
</Text>
</View>
<View style={styles.inputcontainer}>
<TextInput style={styles.input} onChangeText={(text) => this.setState({newTodo: text})} value={this.state.newTodo}/>
<TouchableHighlight
style={styles.button}
onPress={() => this.addTodo()}
underlayColor='#dddddd'>
<Text style={styles.btnText}>Add!</Text>
</TouchableHighlight>
</View>
<ListView
dataSource={this.state.todoSource}
renderRow={this.renderRow.bind(this)} />
</View>
);
}
} // Main Class End

Make sure to create new objects instead of updating the properties of existing objects.
If you want to update listView, create new objects instead of updating
the properties of existing objects.
The below code resolved a similar issue on Github.
let newArray = oldArray.slice();
newArray[indexToUpdate] = {
...oldArray[indexToUpdate],
field: newValue,
};
let newDataSource = oldDataSource.cloneWithRows(newArray);
For more detailed explanation, This answer might help you.

Related

RefreshControll data duplicate everytime do pull to refresh on ScrollView in React native

Description
I implement a pull request using RequestController in React Native, every time I did pull to refresh the same data keep adding on in Flat list over and over. I implemented pull request not inside the Flat list but on the ScrollView which wrapped the FlatList.
Actions
import React, { Component } from 'react';
import { View, StyleSheet, Text, Button, Modal, Dimensions, ScrollView, TextInput, TouchableOpacity, StatusBar, Image, Platform, TouchableNativeFeedback,FlatList, ImageBackground, RefreshControl } from 'react-native';
import axios from 'axios';
class HomeScreen extends Component{
state = {
refreshing: false,
}
componentDidMount(){
this.searchApi();
}
searchApi = async() => {
const response = await axios.get('http://73udkYid.ngrok.io/api/v1/products',{
headers: {
"x-auth-token":"eyJfaWQiOiI1ZdfjzZmM4YjIwYjBjZDQyMmJkNzUiLCJpYXQiOjE2MD"
}
}
);
this.setState({results: [...this.state.results, response.data]});
}
_onRefresh = () => {
this.setState({refreshing: true});
this.searchApi().then(() => {
this.setState({refreshing: false});
});
}
render(){
let finalGames = [];
this.state.results.forEach(obj => {
Object.keys(obj).forEach(key => {
finalGames.push(obj[key]);
});
});
return (
<ScrollView style={{flex: 1,backgroundColor: 'white',}}
refreshControl = {
<RefreshControl
refreshing = { this.state.refreshing }
onRefresh={this._onRefresh}
/>
}
>
<FlatList
data = { finalGames }
keyExtractor = {(item, index) => index.toString()}
renderItem = { ({item: itemData}) => {
if(itemData.empty == true){
return <View style = {[styles.item,styles.itemInvisible]}/>
}
return (
<View style = {{ flex: 1, margin: 4}}>
<View style = {styles.item}>
<TouchableOpacity
onPress = {() => {
this.setState({ viewController: this.state.viewController++ })
this.props.navigation.navigate(
"ProductDetail", {
itemDataDetail: itemData,
businessname:this.props.navigation.state.params.businessname,
viewController: this.state.viewController,
})
}}>
<View>
<ImageBackground
source={{ uri: itemData.photo }}
style={{ width:'100%',aspectRatio: 1, borderRadius: 15, borderWidth:1, borderColor:"#FAFAFA", overflow: 'hidden'}}>
</ImageBackground>
<View style = {{ margin: 5}}>
<Text style = {{color: '#2E2E2E', fontWeight:"bold"}} numberOfLines={1}>{itemData.item}</Text>
<Text style = {{color: '#2E2E2E', fontSize: 12, fontWeight:"normal", alignSelf: 'flex-start'}} numberOfLines={1}>Available now | Sold out</Text>
<Text style = {{color: 'white', fontSize: 18, fontWeight:"bold", backgroundColor:"#DE1F38", alignSelf: 'flex-start', paddingHorizontal: 10,paddingVertical:2,borderRadius: 8,overflow: 'hidden', marginTop: 5}} numberOfLines={1}>${itemData.price}</Text>
</View>
</View>
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
}}/>
}
Output
Data duplicated every time new pull refresh triggered
I assume your api-call returns the whole list of products
This line concat api-response-data to the the list of products you already have in your component-state
this.setState({results: [...this.state.results, response.data]});
Try this instead...
this.setState({ results: response.data });
You should replace your data instead of concatenating. Use:
this.setState({ results: response.data });
Also, you should use FlatList 'onRefresh' prop to implement refresh functionality instead of using an extra ScrollView on the parent.
Oh I found a way. I just need to do this.
this.setState({results: [response.data]});
I was facing the same problem as you,
When I refreshed, the data was (data)+[(data)+(new_data)].
What happens here is that data is added to the array of this variable: results.
To prevent this you must first clear this variable: results.
So your code will look like this.
state = {
refreshing: false,
results : [],
}
when API runs, this array will filled results[{some_data},{some_data},{some_data},..],
While you refresh->
1st: The results will Empty,
2nd: reassign that array with newly added data from API.
_onRefresh = () => {
this.setState({results: []});
this.setState({refreshing: true});
this.searchApi().then(() => {
this.setState({refreshing: false});
});
}

How can I remove an item from a FlatList and then update that list in React Native?

I am making a To Do list app using React Native, where I add events to a FlatList and then have a button that removes that event once it it finished. So far this is what I have. It seems very hacky to me, but most of it works.
import React from 'react';
import { StyleSheet, Text, View, TextInput,TouchableOpacity, FlatList} from 'react-native';
export default class App extends React.Component {
constructor(props){
const data = [];
super(props);
this.state ={
text: 'Enter activity here',
data: data,
color: true,
currNum: 0,
}
}
updateText(){
this.setState({data:this.state.data.concat({key:this.state.text,index:this.state.currNum})});
this.state.currNum++;
}
removeText(item){
this.setState({data:this.state.data.pop(item.index)});
this.state.currNum--;
}
render() {
return (
<View style={styles.container}>
<Text></Text>
<View style = {{flexDirection:'row',justifyContent:'flex-end'}}>
<TextInput style = {{fontSize:30,borderColor:'black', flex:1, marginTop:20}} onChangeText = {(text) => this.setState({text})}value = {this.state.text}/>
<TouchableOpacity style = {{marginTop:20}}onPress = {()=>(this.updateText())}>
<Text>Add to list</Text>
</TouchableOpacity>
</View>
<View style = {{flex:1, flexDirection:'row'}}>
<FlatList
data = {this.state.data}
extraData = {this.state}
renderItem = {({item}) => <View><Text style={styles.text} >{item.key}</Text><TouchableOpacity onPress = {() => this.removeText(item)}><Text>Remove</Text></TouchableOpacity></View>}
/>
</View>
</View>
);
}
}
When I press the "remove" button, I delete an element from the list of data that the FlatList uses. However, whenever I do this I get an error saying "Tried to get frame for out of range index NaN". Is there a way for me to regularly update and remove a FlatList, and to re-render the FlatList once I have removed an item? I have tried using the extraDate prop, but it hasn't worked. I believe I am using it wrong though. Thank you for all the help.
I think you should use .filter
removeText(item){
this.setState({
data: this.state.data.filter((_item)=>_item.key !== item.key)
});
}
And what this.state.currNum - is for?
use this instead you shouldn't mutate this.state and Array.prototype.pop() mutates it
removeText(item, index){
this.setState({data: [
...this.state.data.slice(0, index),
...this.state.data.slice(index + 1)
]});
this.state.currNum--;
}

React Native Flatlist returns the wrong number of empty rows

When I run the code below, it displays 3 empty rows. It should be showing two rows each with a color and enddate and I want to use the 'Parent' as the unique key. The 'Parent' is the unique key created by Firebase when color and enddate were pushed to Firebase with '.push'.
I've tried all sorts of things to get it to display. I did get content to display when I made the 'renderItems' return 'this.state.list', but that returned 3 lines all with the same data, which is the content of the last array on the console log.
I would really appreciate some help to get this working.
Here is the code, a copy of Firebase database and the console.log. Please note that the Firebase 'goal' has been changed to 'color'.
import React, { Component } from 'react';
import { Text, FlatList, View, Image } from 'react-native';
import firebase from 'firebase';
import { Button, Card, CardSection } from '../common';
import styles from '../Styles';
class List extends Component {
static navigationOptions = {
title: 'List',
}
constructor(props) {
super(props);
this.state = {
list: [],
};
}
componentDidMount() {
const { currentUser } = firebase.auth();
const Parent = firebase.database().ref(`/users/${currentUser.uid}/Profile`);
Parent.on(('child_added'), snapshot => {
this.setState({ list: [snapshot.key, snapshot.val().color, snapshot.val().enddate] });
console.log(this.state.list);
});
}
keyExtractor = (item, index) => index;
render() {
return (
<Card>
<View style={{ flex: 1 }}>
<FlatList
data={this.state.list}
keyExtractor={this.keyExtractor}
extraData={this.state}
renderItem={({ item }) => (
<Text style={styles.listStyle}>
{ item.color }
{ item.enddate }
</Text>
)}
/>
</View>
<CardSection>
<Button
style={{
flex: 1,
flexDirection: 'row'
}}
onPress={() => this.props.navigation.navigate('NextPage', { name: 'user' })}
title="Go to next page"
>
Go to next page
</Button>
</CardSection>
</Card>
);
}
}
export { List };
This is the correct way to store the list
componentDidMount() {
const { currentUser } = firebase.auth();
const Parent = firebase.database().ref(`/users/${currentUser.uid}/Profile`);
Parent.on(('child_added'), snapshot => {
const newChild = {
key: snapshot.key,
color: snapshot.val().color,
enddate: snapshot.val().enddate
}
this.setState((prevState) => ({ list: [...prevState.list, newChild] }));
console.log(this.state.list);
});
}
and your keyExtractor
keyExtractor = (item, index) => item.key;

Go to detail view of an article list with React-Native

I'm trying to create list of articles. Currently, I have the first view that show all articles on a list view. And when we click on an article, we must be redirecting on an other view that show the details of this article. But I don't know the way how to get details of articles.
'use strict';
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, ListView, TouchableHighlight,
ActivityIndicator, Image } from 'react-native';
import { StackNavigator } from 'react-navigation';
import Article from './article';
export default class Home extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true
}
}
static navigationOptions = {
title: 'Articles'
}
componentDidMount() {
return fetch('http://www.femininbio.com/json/liste-articles')
.then((response) => response.json())
.then((responseJson) => {
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.setState({
isLoading: false,
dataSource: ds.cloneWithRows(responseJson),
});
})
.catch((error) => {
console.error(error);
});
}
renderRow(rowData) {
return (
<TouchableHighlight id={rowData.id} onPress={() => this.props.navigation.navigate('Article')} underlayColor='#F5FCFF' style={styles.item}>
<View style={styles.itemView}>
<Image style={styles.image} source={{uri: rowData.i.u}}/>
<View style={styles.blockText}>
<Text style={styles.titleArticle}>{rowData.t}</Text>
<Text style={styles.rubrique}>Rubrique : {rowData.r.n}</Text>
</View>
</View>
</TouchableHighlight>
);
}
render() {
if (this.state.isLoading) {
return (
<View style={styles.loading}>
<ActivityIndicator/>
</View>
);
}
return (
<View style={styles.container}>
<ListView
style={styles.list}
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
/>
</View>
);
}
}
So I lunch an other view with the stacknavigator, I think that I must use props to get id of the articles and to filter data but is a little blurry for me.
While navigating to a new screen you can pass parameters to the new screen. With the parameter you get in the new screen you can you can make a new fetch or just use the information you passed to show data on screen. You can read more about on this topic at react-navigation docs
Example
<TouchableHighlight
id={rowData.id}
onPress={() => this.props.navigation.navigate('Article', { article: rowData})}>
// ...
</TouchableHighlight>
// And in your Article Screen
console.log(this.props.navigation.state.params.article) // will print article object/data

React-Native. Can only update a mounted or mounting component

import React, {Component} from 'react'
import {
Image,
AppRegistry,
StyleSheet,
Text,
View,
TextInput,
TouchableOpacity,
ListView,
TouchableHighlight
} from 'react-native'
import ViewContainer from '../../components/ViewContainer';
import StatusbarBackground from "../../components/StatusbarBackground";
import firebase from 'firebase';
export default class Comments extends Component {
constructor(props) {
super(props)
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds.cloneWithRows(['row 1', 'row 2']),
comment: '',
post: '',
}
this.componentDidMount();
this.componentDidMount = this.componentDidMount(this);
this.listenForItems = this.listenForItems.bind(this);
this.renderItem = this.renderItem.bind(this);
this._comment = this._comment.bind(this);
}
componentDidMount() {
var commentsRef = firebase.database().ref('/comments')
this.listenForItems(commentsRef);
}
listenForItems(commentsRef) {
var commentsRef = firebase.database().ref('/comments')
commentsRef.on('value', snap => {
var items = [];
snap.forEach((child) => {
if(child.val().post == this.state.post){
items.push({
post: child.val().post,
email: child.val().email,
comment: child.val().comment,
uid: child.key
});
}
});
var temp = []
var len = items.length;
for (var i = (len - 1); i >= 0; i--) {
temp.push(items[i]);
}
items = temp;
this.setState({
dataSource: this.state.dataSource.cloneWithRows(items)
});
});
}
_comment(post) {
var commentRef = firebase.database().ref('/comments');
var curr = firebase.auth().currentUser.email;
var newComment = commentRef.push();
newComment.set({
'post': post,
'email': curr,
'comment': this.state.comment,
});
}
renderItem(item) {
return (
<TouchableHighlight>
<View style={styles.post}>
<Text style={styles.email}>{item.email}{' said:'}</Text>
<Text style={styles.text}>{item.comment}</Text>
<Text style={styles.line}></Text>
</View>
</TouchableHighlight>
)
}
render() {
this.state.post = this.props.post
return (
<ViewContainer>
<StatusbarBackground />
<Image style={styles.title}
source={require('../../images/comment.png')}
/>
<TextInput
style={styles.textinput}
multiline={true}
placeholder = "Write something..."
onChangeText={(comment) => this.setState({comment: comment})}
value={this.state.comment}
placeholderTextColor = 'black'
underlineColorAndroid = 'white'
autoCorrect = {false}
/>
<View style={styles.comment}>
<TouchableOpacity onPress={() => {this._comment(this.props.post)}}>
<Text>Publish</Text>
</TouchableOpacity>
</View>
<View style={styles.container}>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderItem} />
</View>
</ViewContainer>
)
}
}
I'm making a social app with posts, likes and comments. When I want to see the comments of a post I'm rendering a list view with all the comments. The first try it works but if I want to see the comments of other post I get this error.
I think I have to use componentWillUnmount() but idk what code I have to put there. Any ideias? Thanks!
Remove this.componentDidMount() from your constructor, and remove the line where you bind it. It is called automatically in the React component lifecycle, which is available because you extend Component.
You should also have the componentWillUnmount function that should call something like:
this.commentsRef.off(...)
In order to remove the listener. In order to do that correctly, move the commentsRef callback to its own class function (call it onCommentsRefValue or something), and then you can do this.commentsRef.on('value', this.onCommentsRefValue ) and subsequently in componentWillUnmount you can call this.commentsRef.off('value', this.onCommentsRefValue )

Categories

Resources