React Native: ListView.DataSource is not updating - react-native

I am trying to update ListView.DataSource after fetching data from server but its not happening. I have two components, one is imported in other.
From base component I am trying to update ListView.DataSource in other component. Here is my code.
index.android.js
import React, { Component } from "react";
import { ListView, View, Button, AppRegistry } from "react-native";
import OtherComponent from "./Components/OtherComponent";
class MyApp extends Component {
constructor(props) {
super(props);
this.state = {
movies: [{ title: "ABCD" }, { title: "EFGH" }]
};
}
getTopics = () => {
fetch("https://facebook.github.io/react-native/movies.json")
.then(response => response.json())
.then(responseText => {
console.log(responseText.movies);
this.setState({
movies: responseText.movies
});
})
.catch(error => {
console.warn(error);
});
};
render() {
return (
<View>
<Button
onPress={this.getTopics}
title="Get Topics"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/>
<OtherComponent movies={this.state.movies} />
</View>
);
}
}
AppRegistry.registerComponent("MyApp", () => MyApp);
OtheComponent.js
import React, { Component } from "react";
import { View, ListView, Text, StyleSheet, Button } from "react-native";
export default class FormativeRevisionList extends Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
dataSource: ds.cloneWithRows(props.movies)
};
}
render() {
return (
<View>
<ListView
style={styles.listContainer}
dataSource={this.state.dataSource}
renderRow={rowData => (
<View>
<Text style={styles.listItem}>{rowData.title}</Text>
</View>
)}
/>
</View>
);
}
}
const styles = StyleSheet.create({
listContainer: {
paddingTop: 22
},
listItem: {
fontSize: 30,
fontWeight: "bold",
textAlign: "center"
}
});

In your code, you only set your dataSource in contructor of FormativeRevisionList, this mean FormativeRevisionList will only render the given movies when your first render it.
To render new list after you press Get Topics button , you need to set the dataSource again when it receive new props, this can be achieve by setting it in FormativeRevisionList componentWillReceiveProps
componentWillReceiveProps(nextProps) {
if (nextProps.movies !== this.props.movies) {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(nextProps.movies)
})
}
}
You can read more about componentWillReceiveProps from here

Related

List Item Data is populated but the data does not display on flat list

In this FlatList, the list items Display but they are blank.
I have set break points and the data is there, but it only displays blank list items.
I think the issue may arise when I am setting the array empty, then it does not re-render properly when the fetch is complete.
Crypto Component
import {StyleSheet, FlatList} from 'react-native';
import {getCrypto} from '../api_service/crypto';
import CryptoItem from '../data/cryptoItem';
export default class CryptoMarket extends Component {
constructor(props) {
super(props);
this.state = {cryptos: [], refreshing: true};
this.fetchCrypto = this.fetchCrypto.bind(this);
}
componentDidMount() {
this.fetchCrypto();
}
fetchCrypto() {
getCrypto()
.then(cryptos => this.setState({cryptos, refreshing: false}))
.catch(() => this.setState({refreshing: false}));
}
handleRefresh() {
this.setState(
{
refreshing: true,
},
() => this.fetchCrypto(),
);
}
render() {
return (
<FlatList
data={this.state.cryptos}
renderItem={({item}) => <CryptoItem crypto={{item}} />}
keyExtractor={item => item.name}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh.bind(this)}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0f0f0f',
},
contentContainer: {
marginTop: 50,
alignItems: 'center',
paddingHorizontal: 20,
},
title: {
fontSize: 20,
color: '#fff',
},
});
Fetch API
export async function getCrypto() {
try {
const fetchCrypto = await fetch(
'https://api.nomics.com/v1/currencies/ticker?key=***&ids=BTC,ETH,XRP',
);
const displayCrypto = await fetchCrypto.json();
console.log(displayCrypto);
return displayCrypto;
} catch (error) {
throw error;
}
}
List Item
import React, {Component} from 'react';
import {ListItem} from 'react-native-elements';
export default class CryptoItem extends Component {
render() {
const {name, logo_url, price} = this.props.crypto;
return (
<ListItem
title={name}
text={'Price : ' + price}
leftAvatar={{source: {uri: logo_url}}}
bottomDivider={true}
/>
);
}
}

How to separate axios call from component in react native?

I am new to react native. I have following component in my project for now I have written for fetching API in same component but want to separate it out. I am getting difficulty for how can i access variable which I am using in "getAlbum" method from outside of component.
Is there standard way to separate API call from component?
import React, { Component } from 'react';
import {
FlatList, Text, View, Image, TouchableOpacity,
} from 'react-native';
import { ActivityIndicator, Provider } from 'react-native-paper';
import axios from 'axios';
import styles from '../style/ThumbnailView.component.style';
import ErrorAlert from '../common/ErrorAlert';
import * as myConstant from '../common/Constants';
export default class HomeScreen extends Component {
// For to Navigation header
static navigationOptions = () => ({
headerTitle: 'Album Information',
});
constructor(props) {
super(props);
this.state = {
isLoading: true,
apiLoadingError: false,
};
}
getAlbums() {
const { navigation } = this.props;
const albumId = navigation.getParam('albumID', 'no data');
axios
.get(
myConstant.API + `photos?albumId=${albumId}`, {timeout: myConstant.TIMEOUT}
)
.then((response) => {
this.setState({
isLoading: false,
dataSource: response.data,
});
})
.catch(err => {
this.setState({isLoading: false, apiLoadingError: true})
});
}
componentDidMount() {
this.getAlbums();
}
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 30 }}>
<ActivityIndicator animating={true} size='large' />
</View>
);
}
if (this.state.apiLoadingError) {
return (
<ErrorAlert />
);
}
return (
<React.Fragment>
<Provider>
<View style={styles.listContainer} >
<FlatList
testID='flatlist'
data={ this.state.dataSource } numColumns={3}
renderItem={({ item }) => <View style={styles.listRowContainer}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('AlbumDetailsViewScreen', {
albumTitle: item.title, albumImg: item.url
})} style={styles.listRow}>
<View style={styles.listTextNavVIew}>
<Image source = {{ uri: item.thumbnailUrl }} style={styles.imageViewContainer} />
</View>
</TouchableOpacity>
</View>
}
keyExtractor = { (item, index) => index.toString() }
/>
</View>
</Provider>
</React.Fragment>
);
}
}
You can separate your axios call by making another class with function which will receive 'albumID' as an argument - then add it to your axios link. If you want to call this function from another class just make it static and use like in example below. Then you can map your fetchData to parse it into state. Hope it will help you.
export class Api {
static fetchData = (albumId: string) => {
//here your axios call which will return an array
}
}
export default class HomeScreen extends React.Component {
state = {
//.....
}
receivedData = Api.fetchData('albumID');
//you can map array here to get what you want.
}

Emitting one event cause other event to trigger in react native

Context:
I am rendering a web view and the navigation buttons are on top bar. I send an event when a navigation back button is pressed. And similarly with the forward button it fires an event. The problem is when I press the back button it causes forward button to fire as well. Thus on the console it says, back button pressed and forward button pressed. This behaviour happens in Android only, in ios it works perfect. I am not sure what am I missing in the Android side.
My component is as follows.
import _EventEmitter from 'EventEmitter'
const appEventEmitter = new _EventEmitter()
export { appEventEmitter }
import React, { Component } from 'react'
import {
StyleSheet,
ScrollView,
} from 'react-native'
import { connect } from 'react-redux'
import { WebView } from 'react-native-webview'
import { Linking } from 'react-native'
import Spinner from 'react-native-loading-spinner-overlay'
import { appEventEmitter } from 'src/common'
import { Icon } from 'react-native-elements'
const goBack = 'goBack'
const goForward = 'goForward'
class HomeComponent extends Component {
static navigationOptions = ({navigation}) => {
return {
headerRight:(
<Icon
iconStyle={styles.chevronColor}
name="chevron-right"
onPress={() => appEventEmitter.emit(goForward) }
size={40}
/>),
headerLeft:(
<Icon
iconStyle={styles.chevronColor}
name="chevron-left"
onPress={() => appEventEmitter.emit(goBack) } // This causes to fire back and forward events
size={40}
/>),
}
}
constructor(props) {
super(props);
this.state = {
webViewRef: "webViewRef",
visible: true,
}
}
componentDidMount () {
this.goBackListenerId = appEventEmitter.addListener(goBack, () => this.goBack())
this.goForwardListenerId = appEventEmitter.addListener(goForward, () => this.goForward())
}
componentWillUnmount () {
appEventEmitter.removeListener(this.goBackListenerId)
appEventEmitter.removeListener(this.goForwardListenerId)
}
goBack = () => {
console.log("BACK PRESSED")
this.refs[this.state.webViewRef].goBack();
}
goForward = () => {
console.log("Forward PRESSED")
this.refs[this.state.webViewRef].goForward();
}
hideSpinner() {
this.setState({ visible: false });
}
showSpinner() {
this.setState({ visible: true });
}
render() {
return (
<ScrollView contentContainerStyle={styles.scrollableContainer}>
<Spinner
visible={this.state.visible}
style={styles.spinnerColor}
/>
<WebView
source={{uri: BASE_URL}}
style={styles.container}
onLoadStart={() => this.showSpinner()}
onLoadEnd={() => this.hideSpinner()}
ref={this.state.webViewRef}
javaScriptEnabled={true}
domStorageEnabled={true}
geolocationEnabled={true}
cacheEnabled={true}
/>
</ScrollView>
)
}
}
const styles = StyleSheet.create({
scrollableContainer: {
flex: 1,
},
spinnerColor: {
color: 'white'
},
navigationHeader: {
backgroundColor: colors.primary,
},
container: {
flex: 1,
},
chevronColor: {
color: 'white'
}
});
const Home = connect()(HomeComponent)
export { Home }

Update props from other component in react native

I have a Main class which I show an array to user, then in detail page user can edit each element which I'm passing using react navigation parameter. I want to edit my array in the detail class and save it using async storage.
//Main.jsimport React from 'react';
import {
StyleSheet ,
Text,
View,
TextInput,
ScrollView,
TouchableOpacity,
KeyboardAvoidingView,
AsyncStorage
} from 'react-native'
import Note from './Note'
import detail from './Details'
import { createStackNavigator, createAppContainer } from "react-navigation";
export default class Main extends React.Component {
static navigationOptions = {
title: 'To do list',
headerStyle: {
backgroundColor: '#f4511e',
},
};
constructor(props){
super(props);
this.state = {
noteArray: [],
noteText: '',
dueDate: ''
};
}
async saveUserTasks(value) {
try {
await AsyncStorage.setItem('#MySuperStore:userTask',JSON.stringify(value));
} catch (error) {
console.log("Error saving data" + error);
}
}
getUserTasks = async() =>{
try {
const value = await AsyncStorage.getItem('#MySuperStore:userTask');
if (value !== null){
this.setState({ noteArray: JSON.parse(value)});
}
} catch (error) {
console.log("Error retrieving data" + error);
}
}
render() {
this.getUserTasks()
let notes = this.state.noteArray.map((val,key) => {
return <Note key={key} keyval={key} val={val}
deleteMethod={ () => this.deleteNote(key)}
goToDetailPage= {() => this.goToNoteDetail(key)}
/>
});
const { navigation } = this.props;
return(
<KeyboardAvoidingView behavior='padding' style={styles.keyboard}>
<View style={styles.container}>
<ScrollView style={styles.scrollContainer}>
{notes}
</ScrollView>
<View style={styles.footer}>
<TextInput
onChangeText={(noteText) => this.setState({noteText})}
style={styles.textInput}
placeholder='What is your next Task?'
placeholderTextColor='white'
underlineColorAndroid = 'transparent'
>
</TextInput>
</View>
<TouchableOpacity onPress={this.addNote.bind(this)} style={styles.addButton}>
<Text style={styles.addButtonText}> + </Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
addNote(){
if (this.state.noteText){
var d = new Date();
this.state.noteArray.push({
'creationDate': d.getFullYear() + "/" + (d.getMonth()+1) + "/" + d.getDay(), 'taskName': this.state.noteText,'dueDate':'YYYY/MM/DD'
});
this.setState({noteArray:this.state.noteArray})
this.setState({noteText: ''});
this.saveUserTasks(this.state.noteArray)
}
}
deleteNote(key){
this.state.noteArray.splice(key,1);
this.setState({noteArray: this.state.noteArray})
this.saveUserTasks(this.state.noteArray)
}
goToNoteDetail=(key)=>{
this.props.navigation.navigate('DetailsScreen', {
selectedTask: this.state.noteArray[key],
});
}
}
in detail view I have this method which is similar to add note in main class:
export default class Details extends React.Component {
render() {
const { navigation } = this.props;
const selectedTask = navigation.getParam('selectedTask', 'task');
return(
<View key={this.props.keyval} style={styles.container}>
<TouchableOpacity onPress={this.saveEdit.bind(this)} style={styles.saveButton}>
<Text style={styles.saveButtonText}> save </Text>
</TouchableOpacity>
</View>
);
}
saveEdit(){
let selectedItem = { 'creationDate': selectedTask['creationDate'],
'taskName': selectedTask['taskName'],
'dueDate': this.state.dueData}
this.props.navigation.state.params.saveEdit(selectedItem)
}
}
How can I change my props in any component?
First of all you shouldn't call this.getUserTasks() in the render method because the function has this.setState which is bad and could end in a endless loop I guess or at least effect in worse performance. You could instead call it in componentDidMount:
componentDidMount = () => {
this.getUserTasks();
}
Or alternatively call already in constructor but I prefer the first option:
constructor(props){
super(props);
this.state = {
noteArray: [],
noteText: '',
dueDate: ''
};
this.getUserTasks()
}
this.props.noteArray.push({.. is probably undefined because you aren't passing it down any where. (Didn't see any reference in your snippet). I guess I would implement the saveEdit function in the Main.js component and simply pass it down to the navigation route and call the function in Details component by accessing the navigation state props:
Update
goToNoteDetail=(key)=>{
this.props.navigation.navigate('DetailsScreen', {
// selectedTask: this.state.noteArray[key],
selectedItem: key,
saveEdit: this.saveEdit
});
}
saveEdit(selectedItem){
const selectedTask = this.state.noteArray[selectedItem]
this.state.noteArray.push({
'creationDate': selectedTask['creationDate'],
'taskName': selectedTask['taskName'],
'dueDate': this.state.dueData
});
this.setState({noteArray:this.state.noteArray})
this.setState({dueData: 'YYYY/MM/DD'});
this.saveUserTasks(this.state.noteArray)
}
And then call saveEdit in Details Component:
saveSelectedItem = () => {
const { navigation } = this.props.navigation;
const {selectedItem, saveEdit} = navigation.state && navigation.state.params;
saveEdit(selectedItem)
}

react-native - Navigator and toolbarAndroid

Is there any way to set only one toolbarAndroid to be used on every screen of the application in conjunction with a navigator.
I set up a navigator in index.android.js :
import React, { Component } from 'react';
import {
AppRegistry,
Navigator,
} from 'react-native';
import ContactList from './src/containers/ContactList.js';
class MyIndex extends Component {
render() {
return (
<Navigator
initialRoute={{ name: 'index', component: ContactList }}
renderScene={(route, navigator) => {
if (route.component) {
return React.createElement(route.component, { navigator, ...route.props });
}
return undefined;
}}
/>
);
}
}
AppRegistry.registerComponent('reactest', () => MyIndex);
The first screen displays a contact list :
import React, { Component, PropTypes } from 'react';
import {
Text,
View,
TouchableOpacity,
TouchableHighlight,
ListView,
Image,
ActivityIndicator,
ToolbarAndroid,
} from 'react-native';
import styles from '../../styles';
import ContactDetails from './ContactDetails';
import logo from '../images/ic_launcher.png';
const url = 'http://api.randomuser.me/?results=15&seed=azer';
export default class ContactList extends Component {
static propTypes = {
navigator: PropTypes.object.isRequired,
}
constructor(props) {
super(props);
const datasource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
animating: false,
animatingSize: 0,
jsonData: datasource.cloneWithRows([]),
ds: datasource,
appTitle: 'Test',
appLogo: logo,
};
}
_handlePress() {
this.setState({
animating: true,
animatingSize: 80,
});
return fetch(url)
// convert to json
.then((response) => response.json())
// do some string manipulation on json
.then(({ results }) => {
const newResults = results.map((user) => {
const newUser = {
...user,
name: {
title: `${user.name.title.charAt(0).toUpperCase()}${user.name.title.slice(1)}`,
first: `${user.name.first.charAt(0).toUpperCase()}${user.name.first.slice(1)}`,
last: `${user.name.last.charAt(0).toUpperCase()}${user.name.last.slice(1)}`,
},
};
return newUser;
});
return newResults;
})
// set state
.then((results) => {
this.setState({
appSubTitle: 'Contacts list',
animating: false,
animatingSize: 0,
jsonData: this.state.ds.cloneWithRows(results),
});
});
}
renderRow(rowData: string) {
return (
<TouchableHighlight
onPress={() => {
this.props.navigator.push({
first: rowData.name.first,
component: ContactDetails,
props: {
title: rowData.name.title,
first: rowData.name.first,
last: rowData.name.last,
picture: rowData.picture.large,
thumbnail: rowData.picture.thumbnail,
},
});
}}
>
<View style={styles.listview_row}>
<Image
source={{ uri: rowData.picture.thumbnail }}
style={{ height: 48, width: 48 }}
/>
<Text>
{rowData.name.title} {rowData.name.first} {rowData.name.last}
</Text>
</View>
</TouchableHighlight>
);
}
render() {
const view = (
<View style={styles.container}>
<ToolbarAndroid
logo={this.state.appLogo}
title={this.state.appTitle}
subtitle={this.state.appSubTitle}
style={[{ backgroundColor: '#e9eaed', height: 56 }]}
/>
<ActivityIndicator
animating={this.state.animating}
style={[styles.centering, { height: this.state.animatingSize }]}
/>
<TouchableOpacity
onPress={() => this._handlePress()}
style={styles.button}
size="large"
>
<Text>Fetch results?</Text>
</TouchableOpacity>
<ListView
enableEmptySections
dataSource={this.state.jsonData}
renderRow={(rowData) => this.renderRow(rowData)}
onPress={() => this._handleRowClick()}
/>
</View>
);
return view;
}
}
and the second one displays a contact details :
import React, {
Component,
PropTypes,
} from 'react';
import {
Text,
View,
Image,
ToolbarAndroid,
} from 'react-native';
import styles from '../../styles';
export default class ContactDetails extends Component {
constructor(props) {
super(props);
this.state = {
animating: false,
animatingSize: 0,
appTitle: 'Test',
appLogo: { uri: this.props.thumbnail, height: 56 },
appSubTitle: `Contact Details - ${this.props.first} ${this.props.last}`,
};
}
render() {
return (
<View style={styles.container}>
<ToolbarAndroid
logo={this.state.appLogo}
title={this.state.appTitle}
subtitle={this.state.appSubTitle}
style={[{ backgroundColor: '#e9eaed', height: 56 }]}
/>
<Image
source={{ uri: this.props.picture }}
style={{ height: 128, width: 128 }}
/>
<Text>{this.props.title} {this.props.first} {this.props.last}</Text>
</View>
);
}
}
ContactDetails.propTypes = {
title: PropTypes.string.isRequired,
first: PropTypes.string.isRequired,
last: PropTypes.string.isRequired,
picture: PropTypes.string.isRequired,
thumbnail: PropTypes.string.isRequired,
};
I set up an toolbarAndroid in my first screen and another in my second screen, it's working well, but I have a feeling that it would be better to define only one toolbarAndroid and update it calling setState.
Is it possible, if so how ?
Wrap your Navigator class with your ToolbarAndroid. This way, everything that is rendered on the Navigator will be wrapped by the Toolbar. Actually, everything that is common between those scenes should be put on a single component wrapping the rest.
class MyIndex extends Component {
render() {
return (
<ToolbarAndroid>
<Navigator
initialRoute={{ name: 'index', component: ContactList }}
renderScene={(route, navigator) => {
if (route.component) {
return React.createElement(route.component, { navigator, ...route.props });
}
return undefined;
}}
/>
</ToolbarAndroid>
);
}
}
I managed to do that by wrapping the toolbarAndroid and the navigator in a view :
class MyIndex extends Component {
constructor(props) {
super(props);
this.state = {
appTitle: 'Test',
appLogo: logo,
};
}
render() {
return (
<View
style={styles.container}
>
<ToolbarAndroid
logo={this.state.appLogo}
title={this.state.appTitle}
subtitle={this.state.appSubTitle}
style={[{ backgroundColor: '#e9eaed', height: 56 }]}
/>
<Navigator
initialRoute={{ name: 'index', component: ContactList }}
renderScene={(route, navigator) => {
if (route.component) {
return React.createElement(route.component, { navigator, ...route.props });
}
return undefined;
}}
/>
</View>
);
}
}