How to update React-Native listview? - javascript

I am having strange behavior when a row is being added to my listview. I am not sure where my logic is wrong. I have tabs using react-native-tab-view. On Tab 2 I am adding a object to "Favorites" array that has is being displayed on Tab 4. Before I add the object this is what is being displayed
After I add the object and go back to Tab 4 this is what I am see
And Code
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
constructor(props) {
super(props)
this.state = {
favoriteDB: props.favorites,
renderPlaceholder: true,
dataSource: ds.cloneWithRows([])
}
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({
dataSource: ds.cloneWithRows(this.state.favoriteDB),
renderPlaceholder: false
})
})
}
componentWillReceiveProps(prevProps) {
const {favorites} = this.props
if (JSON.stringify(favorites) != JSON.stringify(prevProps.favorites)) {
this._updateFavorites()
}
}
_updateFavorites() {
const {favorites} = this.props
this.setState({
favoriteDB: favorites,
dataSource: ds.cloneWithRows(favorites)
})
}
render() {
const {dataSource} = this.state
const {visibleHeight, visibleWidth} = this.props
if (this.state.renderPlaceholder)
return <Placeholder/>
return (
<Container theme={theme}>
<View style={{ flex:1, height: visibleHeight - 100, width: visibleWidth }}>
<Row>
<Col>
<List>
<ListView
style={{ height: visibleHeight - 100, width: visibleWidth }}
enableEmptySections={TRUE}
automaticallyAdjustContentInsets={FALSE}
initialListSize={100}
pageSize={100}
dataSource={dataSource}
renderRow={rowData => this._renderRow(rowData)}/>
</List>
</Col>
</Row>
</View>
</Container>
)
}

I experienced this adding and removing rows to the input of cloneWithRows with ListView as well as the data array for the new SectionList.
My workaround is to add a key for ListView that changes when the dataSource changes (name and size worked).
return (
<SectionList
key={"mylistview_" + this.props.maps.length}
style=
...
See https://stackoverflow.com/a/31481960/7223

Related

View not re-rendering after onPress

I'm trying to change the backgroundColor of a React Native Card component when onPress event is triggered. Although I'm seeing the change of the state on componentDidUpdate, I'm not visualising it.
I'm changing the value of the itemsPressed array when the onPress event is triggered. If the pressed item id is already in the array it removes it else it adds it into the array.
export default class Popular extends Component {
constructor(props) {
super(props);
this.togglePressed = this.togglePressed.bind(this);
this.state = {
categories: [],
itemsPressed: []
}
}
togglePressed = item => {
const id = item.id;
this.setState(({ itemsPressed }) => ({
itemsPressed: this.isItemPressed(item)
? itemsPressed.filter(a => a != id)
: [...itemsPressed, id],
}))
};
isItemPressed = item => {
const id = item.id;
return this.state.itemsPressed.includes(id);
};
componentDidMount() {
this.setState({
categories:this.props.categories,
});
}
componentDidUpdate(){
console.log(this.state.itemsPressed);
}
renderTabItem = ({ item,index }) => (
<TouchableOpacity
style={styles.category}
key={index}
onPress={() => this.togglePressed(item)}
>
<Card center
style={[styles.card,{backgroundColor:
this.isItemPressed(item)
? item.color
: 'gray'
}]}>
<Image source={item.icon} style={styles.categoryIcon}/>
</Card>
<Text size={12} center style={styles.categoryName}
medium color='black'
>
{item.name.toLowerCase()}
</Text>
</TouchableOpacity>
);
renderTab(){
const {categories} = this.state;
return (
<FlatList
horizontal = {true}
pagingEnabled = {true}
scrollEnabled = {true}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
snapToAlignment='center'
data={categories}
keyExtractor={(item) => `${item.id}`}
renderItem={this.renderTabItem}
/>
)
}
render() {
return (
<ScrollView>
{this.renderTab()}
</ScrollView>
);
}
}
I expected a visual change but I couldn't re render the renderTab().
Thank you!
Your FlatList has the property category as data source, so it only re-renders the cells if it detects a change in the category property. Your code however is only changing itemsPressed, so no cell is re-rendered.
You can tell the FlatList to listen for changes state.itemsPressed by specifying it in the extraData property:
extraData={this.state.itemsPressed}

How to call a child method from the parent in React Native?

When a click event is fired within my parent component I need to call the method closeMenu() from the SearchBar child component. I have tried a couple of different ways to do that but none of them are working. Does anyone know how to do this?
class Products extends Component {
constructor(props) {
super(props);
this.state = { closeMenu: false};
this.hideSearchBar = this.hideSearchBar.bind(this);
}
hideSearchBar(e) {
console.log('e: ', React.Children)
this.setState({closeMenu: true});
this.refs.SearchBar.closeMenu();
this.setState({closeMenu: false});
}
componentWillMount() {
this.props.dispatch(getProductList());
}
render() {
const {isLoading, products} = this.props.products;
if (isLoading) {
return <Loader isVisible={true}/>;
}
return (
<TouchableWithoutFeedback onPress={(e) => this.hideSearchBar(e)} style={{zIndex: 0}}>
<View style={styles.wrapper}>
<Header/>
<View style={styles.bodyWrapper}>
<ScrollView style={styles.scrollView}>
<ProductsContainer data={{productsList: { results: products }}}/>
</ScrollView>
<SearchBar ref="SearchBar" style={styles.searchBar} />
</View>
<Footer/>
</View>
</TouchableWithoutFeedback>
);
}
}
I also tried calling closeMenu() without refs:
hideSearchBar(e) {
this.setState({closeMenu: true});
this.SearchBar.closeMenu();
}
Here is the SearchBar component:
class SearchBar extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.suggestions = [];
}
componentWillUpdate(nextProps, nextState) {
console.log("COMPONENT WILL UPDATE");
console.log(nextProps);
console.log(nextState);
}
suggestionClick = (value) => {
}
getSuggestionText = (suggestion) => {
}
onChangeText = (value) => {
this.selectedSuggestion = false
this.props.dispatch(handleSearchItemText(value));
console.log(this.props.products.searchResults);
}
onFocus() {
const {height} = Dimensions.get('window');
this.setState({
contentOffset: {
x: 0,
y: height * 1 / 3
}
});
}
onBlur() {
this.setState({
contentOffset: {x: 0, y: 0}
});
}
closeMenu = () => {
this.props.products.searchResults = {};
}
componentWillReceiveProps() {
if (!this.props.closeMenu) {
this.props.closeMenu = false;
}
}
renderSearches = () => {
this.suggestions = this.props.products.searchResults;
const suggestionTexts = Object.keys(this.props.products.searchResults || {})
console.log(suggestionTexts);
if (!suggestionTexts.length) {
return null
}
// for android absolute element: https://github.com/facebook/react-native/issues/16951
// https://gist.github.com/tioback/6af21db0685cd3b1de28b84981f31cca#file-input-with-suggestions-L54
return (
<View
ref="suggestionsWrapper"
style={autoStyles.suggestionsWrapper}
>
{
this.suggestions.map((text, index) => (
<TouchableHighlight
key={index}
suggestionText={text}
activeOpacity={0.6}
style={autoStyles.suggestion}
onPress={this.suggestionClick(this.suggestions[text])}
underlayColor='white'
>
<Text style={autoStyles.suggestionText}>
{text}
</Text>
</TouchableHighlight>
))
}
</View>
)
}
render() {
const myIcon = (<Icon name="search" size={30} style={styles.searchIcon}/>);
const slidersIcon = (<Icon name="sliders" size={30} style={styles.slidersIcon}/>);
return (
<TouchableWithoutFeedback style={{zIndex: 0}}>
<View style={[styles.searchBar, this.props.style]}>
<View style={styles.searchContainer}>
<View>
{slidersIcon}
</View>
<View style={styles.search}>
<View style={styles.searchSection}>
{myIcon}
<TextInput
style={styles.input}
placeholder="Search"
placeholderTextColor="rgba(0,0,0,0.7)"
onChangeText={(searchString) => {
this.setState({searchString})
}}
underlineColorAndroid="transparent"
editable={true}
autoCorrect={false}
autoFocus={false}
autoCaptialize={'none'}
autoCorrect={false}
onChangeText={this.onChangeText}
enablesReturnKeyAutomatically={true}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
/>
</View>
</View>
</View>
{this.renderSearches()}
</View>
</TouchableWithoutFeedback>
);
}
}
There are some issues which you should avoid:
Never mutate props: this.props.something = {} is an anti-pattern. Think about props as data that your component does not own and which are not mutable. If they change then only because the parent passed new props.
Also you have multiple handlers in your SeachBar that are not bound to this but use this. It will not work. Use arrow functions if you want to use this or bind them in the constructor.
You should overthink the architecture of your app. Maybe it is a good idea to split the search bar and the result list into two separate components. When the user types something to search for update your redux store and display the results in a separate component that you only render if there are search results.
I'm affraid it would exceed the length of a stackoverflow answer to solve all these issues. Maybe you should go back to the basics first and do the really good redux tutorial.

Update list in a component when database has updated React

I need some help to think here. I now get and displays a list of user from my database depending on if the user is set to true or false.
And this works fine, but when i change a user from true to false in the database i would like to update the list that is displayed.
if i re render the page componetWillMount will do the job for me, but i would like to update the list without re render it.
I´ve looked and tried with componentWillUpdate but cant make it work.
class activeUser extends Component {
// Call fetchList to get access to the users
componentWillMount () {
this.props.fetchList()
this.createDataSource(this.props)
}
componentWillReceiveProps (nextProps) {
// nextProps are the next set of props that this component
// will be rendered with
// this.props is still the old set of props
this.createDataSource(nextProps)
}
createDataSource ({ users }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
})
this.dataSource = ds.cloneWithRows(users)
}
// Render out the users
renderRow (user) {
return <ListUserItem user={user} />
}
render () {
return (
<View>
<View style={{ height: 320, justifyContent: 'center', alignItems:
'center', marginTop: 35 }} >
<ListView
showsVerticalScrollIndicator={false}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
</View>
</View>
)
}
}
const mapStateToProps = state => {
const users = _.map(state.list, (val) => {
return {...val}
})
return { users }
}
export default connect(mapStateToProps, {fetchList})(GoingOut)

Calling multiple functions to render

First of all, I want to explain what I want to do and after where my problem is.
Initially I do a fetch request to load some data, since here is working okey.
After with one value of this data I want to do another fetch. For that I call another function renderImage(), where I do the fetch and I render the code that I want.
My problem is that is not rendering nothing, because is not changing the status into loaded. And of course fetch is asynchronous and needs more time.
And I don't know how do it that it works and more simple because I thinks that I doing it a little bit complicated.
This is my code:
class Dashboard extends Component{
constructor(props){
super(props)
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
}),
loaded: false,
datos: '',
}
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch(REQUEST_URL)
.then((response) => response.json())
.then ((responseData) =>{
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData),
})
})
}
renderLoadingView(){
return(
<View>
<Text>Cargando...</Text>
</View>
)
}
async renderImage(receta){
const REQUEST_URL = "yyyyyyyy" + receta.user_id;
const response = await fetch(REQUEST_URL);
const json = await response.json();
this.setState({ loaded : true});
return(
<Card >
<CardItem>
<Left>
<TouchableOpacity>
<Thumbnail style={{width: 50, height: 50, borderRadius: 25}} source={{uri: json.imageUrl}} />
</TouchableOpacity>
<Body>
<Text>{receta.Titulo}</Text>
<Text>{receta.Username}</Text>
</Body>
</Left>
</CardItem>
</Card>
)
}
renderReceta(receta){
return this.renderImage(receta);
}
render(){
if(!this.state.loaded){
return this.renderLoadingView();
}
else{
return(
<Container>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderReceta.bind(this)}
/>
</Container>
)
}
}
}
Perhaps not an "answer" but the question is a bit vague. This can be solved in many ways.
Option one:
Load the array first, then show the list, and async "sideload" each row's images.
Can be bad if you have a lot of images so watch out for that. Also you might load images here that you might never render (they are out of view, e.g. the user never scrolls to them), but you also wont load them twice to... pros and cons.
class Dashboard extends Component{
constructor(props){
super(props)
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
}),
loaded: false,
datos: '',
images: {
}
}
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch(REQUEST_URL)
.then((response) => response.json())
.then ((responseData) =>{
this.setState({
loaded: true,
dataSource: this.state.dataSource.cloneWithRows(responseData),
});
this.fetchImageUrls(responseData);
});
}
fetchImageUrls(responseData){ //assuming respons data is an array of receta objects
responseData.forEach(({user_id})=>{
fetch("wwwsomewhere").then(r => r.json()).then(({imageUrl})=>{
this.setState({
images: Object.assign(this.state.images, {
[user_id]: imageUrl
})
});
});
});
}
renderLoadingView(){
return(
<View>
<Text>Cargando...</Text>
</View>
)
}
renderImage(receta){
const {Titulo, Username, user_id} = receta;
return(
<Card >
<CardItem>
<Left>
<TouchableOpacity>
{this.state.images[user_id] ?
<Thumbnail style={{width: 50, height: 50, borderRadius: 25}} source={{uri: this.state.images[user_id]}} />
: "Loading (load thumb here?)"
}
</TouchableOpacity>
<Body>
<Text>{receta.Titulo}</Text>
<Text>{receta.Username}</Text>
</Body>
</Left>
</CardItem>
</Card>
)
}
renderReceta(receta){
return this.renderImage(receta);
}
render(){
if(!this.state.loaded){
return this.renderLoadingView();
}
else{
return(
<Container>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderReceta.bind(this)}
/>
</Container>
)
}
}
}
Options 2:
Bundle upp all your loading into one and after resolve rerender.
class Dashboard extends Component{
constructor(props){
super(props)
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
}),
loaded: false,
datos: '',
recetas: {
}
}
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch(REQUEST_URL)
.then((response) => response.json())
.then ((responseData) =>{
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData),
});
this.fetchImageUrls(responseData);
});
}
fetchImageUrls(responseData){ //assuming respons data is an array of receta objects
//Load all images
Promise.all(responseData.map(({user_id})=>{
return fetch("wwwsomewhere").then(r => r.json());
})).then((recetasArray)=>{
//When all thumb objects (Recetas) have been resolved
//map over the receta object array and create a hash (so you can access them by id later)
this.setState({
loaded: true,
recetas: recetasArray.reduce((acc, receta)=>{
acc[recept.user_id] = receta;
return acc;
},{})
});
});
}
renderLoadingView(){
return(
<View>
<Text>Cargando...</Text>
</View>
)
}
renderImage(receta){
const {Titulo, Username, user_id} = receta;
return(
<Card >
<CardItem>
<Left>
<TouchableOpacity>
<Thumbnail style={{width: 50, height: 50, borderRadius: 25}} source={{uri: this.state.recetas[user_id]}} />
</TouchableOpacity>
<Body>
<Text>{receta.Titulo}</Text>
<Text>{receta.Username}</Text>
</Body>
</Left>
</CardItem>
</Card>
)
}
renderReceta(receta){
return this.renderImage(receta);
}
render(){
if(!this.state.loaded){
return this.renderLoadingView();
}
else{
return(
<Container>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderReceta.bind(this)}
/>
</Container>
)
}
}
}
Untested code but just wanted to share my ideas.

Update Listview which contains Row and Section

I have 2 components which are CalendarList and CalendarListItem. I use Listview to displays 10 records in CalendarListItem at the first load and it will add more 10 records when user scroll down.
I meet trouble when try to update Listview containing Row and Section to append next 10 records when user scroll down to the end of list. I have searched and applied hints but most of questions relating with rows only that not work for me.
Here is my code:
constructor(props) {
super(props);
this.ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
this.state = {
eventData: [],
eventDatas: [],
dataBlob: {},
dataSource: {},
dataBlog: [],
refreshing: false,
page: 1
};
}
componentWillMount() {
this.loadEventData();
}
componentDidMount() {
this.loadEventData();
}
loadEventData() {
eventState = this.props.eventtop;
console.log('NEW DATA', eventState);
const tempDataBlob = this.state.dataBlob;
this.state.eventData = this.props.eventtop;
this.state.eventData.forEach(item => {
const event = item;
const date = new Date(item.EventDate);
const group = dateFormat(date, 'mmmm yyyy');
if (currentGroup !== group) {
currentGroup = group;
currentIndex++;
this.state.eventDatas.push({ divider: currentGroup, data: [] });
}
this.state.eventDatas[currentIndex].data.push(event);
tempDataBlob[this.state.eventDatas[currentIndex].divider] = this.state.eventDatas[currentIndex].data;
this.setState({
dataBlob: tempDataBlob
});
});
this.setState({
dataSource: this.ds.cloneWithRowsAndSections(this.state.dataBlob)
});
console.log('FUCKING DATA', this.state.dataBlob);
}
renderRow(rowData) {
return (
<TouchableOpacity
onPress={() => {
if (window.width <= 600) {
Actions.calendarDetail({ eventtop: rowData });
} else if (window.width > 600) {
this.props.getEventd(rowData);
this.passState.bind(this.props.eventItems);
}
}}
>
<View>
<Text style={styles.evenType}>
{rowData.EventType}
</Text>
<Text style={{ ...styles.contentStyle, ...styles.contenStyle2 }}>
{dateFormat(rowData.EventDate, 'mm/dd/yyyy')}
</Text>
<Icon
name='chevron-thin-right'
style={styles.iconStyle}
/>
<View
style={styles.titleStyle}
>
<Text style={styles.contentStyle}>
{rowData.Title}
</Text>
</View>
</View>
</TouchableOpacity>
);
}
renderSectionHeader(sectionData, sectionId) {
return (
<View>
<Text style={styles.timeStyle}>
{sectionId}
</Text>
</View>
);
}
render() {
return (
<ListView
style={{ flex: 1 }}
enableEmptySections
dataSource={this.state.dataSource}
renderRow={data => this.renderRow(data)}
renderSectionHeader={this.renderSectionHeader}
onEndReached={this.props.onEndReached}
renderFooter={this.props.renderFooter}
onEndReachedThreshold={30}
/>
);
}
I found a solution yesterday. Actually I need to create another Action Creator and place it inside CalendarListItem, that action will return state which is new data then I concat old data with that new data. Done!

Categories

Resources