Edit state of every item of a FlatList - react-native

I made a page in which I use a FlatList. This FlatList uses an item component I made that display another view below itself when pressed by setting a state "hidden" to false. The main issue I have is that I can't find a way to change the "hidden" state to true when one of the item is pressed, hence always keeping only 1 item displaying the additional view at the time. In the same time, when I refresh/re-render my FlatList, it does not set all the "hidden" state back to true.
This is where I render my FlatList
_onRefresh() {
this.setState({refreshing: true}, () => this._loadList());
}
render() {
return (
<View style={[style.container, style.whiteBackground]}>
<CategoryFilter filterCallback={this._changeCategory}/>
<FlatList
data={this.state.list}
extraData={this.state}
renderItem={({item}) =>
<ListItemComponent item={item} category={this.state.category}/>
}
refreshing={this.state.refreshing}
onRefresh={() => this._onRefresh()}
/>
</View>
);
}
And this is where I render and display the hidden view
constructor(props) {
super(props);
this.state = {
hidden: true
};
}
componentDidMount() {
this.setState({hidden: true});
}
_onPress() {
this.setState({
hidden: !this.state.hidden
});
}
[...]
_renderOS(item) {
if (Platform.OS === 'android') {
return (
<TouchableNativeFeedback onPress={() => this._onPress()}>
{this._renderItem(item)}
</TouchableNativeFeedback>
);
} else if (Platform.OS === 'ios') {
return(
<TouchableOpacity onPress={() => this._onPress()}>
{this._renderItem(item)}
</TouchableOpacity>
);
}
}
[...]
_renderDescription(item) {
if (this.state.hidden === true) {
return null;
} else {
return (
<View style={listItemStyle.descriptionContainer}>
<Text style={listItemStyle.description}>
{item.description}
</Text>
</View>
);
}
}
I just want to be able to have only one of the list item with hidden set to false at the time and have said item to be set to hidden=true when the page is refreshed, but I never found anything that could help me.

So after thinking a lot I finally found a solution.
Instead of handling the hidden state in every item, I made a list of every hidden state associated to the items ids in the component where my flatlist is, adding a function that will set the previously opened item to hidden and open the new one, and passing it as a callback to my items so that it can be called when I press them.
_onPress(id) {
let items;
items = this.state.items.map((item) => {
if (item.id === this.state.openId)
item.open = false;
else if (item.id === id)
item.open = true;
return item;
});
this.setState({
items: items,
openId: (id === this.state.openId ? '' : id)
});
}
<FlatList
data={this.state.items}
extraData={this.state}
renderItem={({item}) =>
<ListItemComponent
onPress={this._onPress.bind(this)}
bet={item}
categoryList={this.state.categoryList}
open={item.open}/>
}
refreshing={this.state.refreshing}
onRefresh={() => this._onRefresh()}
/>

Related

TextInput focus blurring when first clicked

There was no issue before I tried to implement the handleInputBlur and handleInputFocus functions (Used to change the background when in focus). When I first click on the TextInput it comes into focus, but then immediately blurs, resulting in the background flashing then disappearing. What's strange is that after this first click, the future clicks work absolutely fine, focusses and blurs as it should. I do not understand why on the initial click/focus it immediately blurs. Code below:
EDIT: Bit more context, it's inside of a modal, which contains multiple of these editable items.
class EditableItem extends Component {
constructor(props) {
super(props)
const { value } = this.props
this.state = {
value,
isFocused: null,
}
}
handleInputBlur = () => {
this.setState({ isFocused: false })
console.log('blurring')
}
handleInputFocus = () => {
this.setState({ isFocused: true })
console.log('focussing')
}
render() {
const { name, secure, children, autoCapitalize } = this.props
const { value, isFocused } = this.state
const multiline = !secure
return (
<View>
<View style={styles.container}>
<Text style={styles.name}>{name}</Text>
<View style={isFocused ? styles.activeBackground : styles.unfocusedBackground}>
<TextInput
placeholder={name}
placeholderTextColor={COLOR_BASE_3}
underlineColorAndroid="transparent"
style={styles.value}
secureTextEntry={secure}
value={value}
// blurOnSubmit
onSubmitEditing={() => {
Keyboard.dismiss()
}}
returnKeyType="done"
keyboardAppearance="dark"
autoCapitalize={autoCapitalize}
onChangeText={this.onChange}
multiline={multiline}
onBlur={() => this.handleInputBlur()}
onFocus={() => this.handleInputFocus()}
/>
{children}
</View>
</View>
<View style={styles.divider} />
</View>
)
}
onChange = value => {
const { onChange } = this.props
this.setState({ value })
onChange(value)
}
}
Ok so solved this by setting autofocus to true in the TextInput. Not sure why not having this set causes this issue but it's a solution regardless.

React-Native-Component not rendering when state is changed

I am making the show more and show less functionality inside a flat list but the state pressed is not working as expected .When I am setting the state value component is not being rendered when the state changes its value.
My constructor is set like below
this.state = {
accList: [],
expanded: false,
expandedText: "Show More"
}
In componentdidmount() I am updating the value of accList value like below
componentDidMount = () => {
this.setState({
accList:[{
"customer_name": "Shubhangi J Thakur",
"message":"Hello"
},
{
"customer_name": "Arthur S Campbell",
"message":"Hello_World"
},
{
"customer_name": "Susan R Brill",
"message":"hellow"
}]
});
}
I have defined the flatlist in render() like below
<FlatList
onScroll={this.handleScroll}
data={this.state.accList}
renderItem={this.renderItem}
keyExtractor={this._keyExtractor}
/>
renderItem = ({ item, index }) => (
<Card style={style.cardLayout} key={index}>
<CardItem header>
<Text>{item.customer_name}</Text>
</CardItem>
{this.seemorefunctionality(item)}
</Card>
);
seemorefunctionality = (item) => {
return <View>
{this.state.expanded ? this.expandedView(item) :null}
//To view Show More and Show Less Text
<TouchableOpacity onPress={this.expandedText}>
<Text> {this.state.expandedText}</Text>
</TouchableOpacity>
</View>
}
}
expandedText = () => {
console.log('Setting the expanded text value', this.state.expanded)
if (this.state.expanded) {
this.setState({
expandedText: "Show More"
});
}
else {
this.setState({
expandedText: "Show Less"
});
}
value=!this.state.expanded
this.setState({
expanded: value
});
}
expandedView = (item) => {
return <View>
{item.map((obj, index) => {
return (
<View key={index} >
<Text>{obj.message}</Text>
</View>
)
})}
</View>
When I am clicking on the this.state.expandedText value is getting changed when we see in the console but its not reflecting in the View also expandedView is not being rendered when this.state.expanded is set to true.
In View the value of this.state.expandedText is always showing Show More while I can see In console that the value is getting changed to Show more and Show Less on click
for re-rendering flatlist you have to add extraData={this.state} as mention on https://facebook.github.io/react-native/docs/flatlist

React native Unable to render the page with force update or set state

class Wait extends Component{
constructor(props) {
super(props);
this.state = { fetchingData: true, data: [], check: ''}
this.forceUpdateHandler.bind(this);
}
getData = async() => {
try {
data = await AsyncStorage.getItem('restaurants');
if (data != null) {
this.setState({fetchingData: false , data: JSON.parse(data)})
}
} catch(error){
console.log(error)
}
}
forceUpdateHandler(){
this.forceUpdate();
};
componentDidMount(){
this.getData();
}
renderRestaurant(){
return this.state.data.map((item) => {
return (
<View style ={{marginTop: 20, backgroundColor: 'red', marginTop: 20 }}>
<Text> {item.name} </Text>
<Text> {item.time} </Text>
<Text> {item.wait} </Text>
<Text> {item.people} </Text>
<Button title = 'cancel' onPress = { async () => {
let data = await AsyncStorage.getItem('restaurants');
let temp = JSON.parse(data)
let i = -1
temp.map((value, index) => {
if (value.name == item.name){
i = index;
}
})
if (i > -1){
temp.splice(i, 1)
await AsyncStorage.setItem('restaurants', JSON.stringify(temp))
}
this.forceUpdateHandler() // First way
this.forceUpdate() // Second way
this.setState({check: 'checked'}) // Third way
}
}
/>
</View>
)
})
}
render(){
const { navigate } = this.props.navigation;
const { navigation } = this.props;
return (
<View style={{width:200, height:200, justifyContent:'center', alignItems:'center', }}>
{this.state.fetchingData ? null : this.renderRestaurant()}
</View>
)
}
}
I am trying to make the page re-render each time after I click the button. Once click the button, it access the AsyncStorage and delete the corresponding element in the array, then it update the AsyncStorage with the new array and re-render the page.
I have tried the following:
1) call forUpdate directly after the update of the AsyncStorage
2) define the forceUpdateHandler function and bind it with this
3) call this.setState after the update of the AsyncStorage
But none of the above options re-renders the page. Can someone help to fix it? An example would be great! Thanks in advance.
The answer is simple. It doesn't re-render because it has nothing to re-render. It calls the render, check each component in the render if the data used to render it has changed and render them if needed. If you look at your code, you see that on the button press, you save in the async storage the new data. However, your rendering uses this.state.data to render the item. The problem is that you never update the state of your component with the new data.
Sure, you do this.setState({check: 'checked'}), but nothing in the render is using it. So there's no point in updating the UI.
An easy way to fix it would be to call this.getData() at the end of the button onPress. That way, you would update the data state which would update your UI.
Get the updated list of restaurants { removing the selected restaurant}
Stored the updated list to Asyncstorage.
fetch the updated list from asyncStorage and set the state.
storeData = async (restaurants) => {
try {
await AsyncStorage.setItem('restaurants', JSON.stringify(restaurants))
} catch (error) {
console.log("Error", error);
}
}
renderRestaurant(){
return this.state.data.map((item, index, restaurants) => {
return (
<View key={index}>
<Text> {item.name} </Text>
<Text> {item.time} </Text>
<Button
title = 'cancel'
onPress = {() => {
let restaurantListWithoutCurrentRestaurant = restaurants.filter((restaurant)=> restaurant.name !== item.name);
this.storeData(restaurantListWithoutCurrentRestaurant);
this.getData();
}}/>
</View>
)
})
}
I think this will solve your problem.

Handle Multiselect in a GridView

I'm trying to handle the multi-select with react-native-super-grid , here is my code :
<GridView
itemDimension={80}
items={items}
style={styles.gridView}
renderItem={item => (
<View style={[styles.itemContainer , { backgroundColor:' transparent '}]}>
<TouchableHighlight style={styles.buttonStyle} onPress={() => this.pressEvent() }>
<Text> {item.image}</Text>
</TouchableHighlight>
<Text style={styles.buttonText}> {item.name}</Text>
</View>)}
/>
I tried using this function :
pressEvent(arr){
if(this.state.pressStatus == false){
this.setState({ pressStatus: true})
this.state.arr.push(arr)
this.setState({ color : 'white'})
} else {
this.setState({ pressStatus: false})
this.setState({ color: 'red'})
}
}
but it somehow doesn't work , can someone help me ?
Thank you .
This short example should give you an idea what are you doing wrong. The items itself are not aware of the state. So what I would do, I would create a separate child component for grid item and handle press state locally. Then handle parent, which is holding all the item trough callback about the pressed item.
class MyGridView extends Component {
render() {
return (
<GridView
itemDimension={80}
items={items}
style={styles.gridView}
renderItem={item => (
<GridItem
item={item}
onItemPress={selected => {
// set grid view callback
if (selected) {
//if true add to array
this.addToPressedArray(item);
} else {
//false remove from array
this.removeFromPressedArray(item);
}
}}
/>
)}
/>
);
}
// You don't change the state directly, you mutate it trough set state
addToPressedArray = item => this.setState(prevState => ({ arr: [...prevState.arr, item] }));
removeFromPressedArray = item => {
const arr = this.state.arr.remove(item);
this.setState({ arr });
};
}
And the GridItem
class GridItem extends Component {
// starting local state
state = {
pressStatus: false,
color: 'red'
};
// handle on item press
pressEvent = () => {
this.setState(prevState => ({
pressStatus: !prevState.pressStatus, //negate previous on state value
color: !prevState.pressStatus ? 'white' : 'red' //choose corect collor based on pressedStatus
}));
// call parent callback to notify grid view of item select/deselect
this.props.onItemPress(this.state.pressStatus);
};
render() {
return (
<View style={[styles.itemContainer, { backgroundColor: ' transparent ' }]}>
<TouchableHighlight style={styles.buttonStyle} onPress={() => this.pressEvent()}>
<Text> {item.image}</Text>
</TouchableHighlight>
<Text style={styles.buttonText}> {item.name}</Text>
</View>
);
}
}
I also recommend to read about React.Component lifecycle. Its a good reading and gives you a better understanding how to achieve updates.
Since GridView has been merged into FlatGrid. Therefore, I've implemented the multi-select option in a pretty easy way. First of all I applied TouchableOpacity on top of the view in the renderItems prop of FlatGrid like this.
<TouchableOpacity
onPress={() => this.selectedServices(item.name)}>
...props
</TouchableOpacity>
SelectedServices:
selectedServices = item => {
let services = this.state.selectedServices;
if (services.includes(item) == false) {
services.push(item);
this.setState({ selectedServices: services });
} else {
let itemIndex = services.indexOf(item);
services.splice(itemIndex, 1);
this.setState({ selectedServices: services });
}
};
Using splice, indexOf, and push, you can easily implement multi-selection.
To change the backgroundColor of the currently selected item, you can apply a check on the backgroundColor prop of the view.
renderItem={({ item, index }) => (
<TouchableOpacity
onPress={() => this.selectedServices(item.name)}
>
<View
style={[
styles.itemContainer,
{
backgroundColor: this.state.selectedServices.includes(
item.name
)
? '#0052cc'
: item.code
}
]}
>
<Text style={styles.itemName}>{item.name}</Text>
</View>
</TouchableOpacity>
)}

React native infinite scroll with flatlist

I followed this tutorial https://www.youtube.com/watch?v=rY0braBBlgw
When I scroll down it sends the request then it gets stuck in a loop and just requests and requests. I think this is a problem with the scrollview in the listview.
I am not sure if you were able to resolve this but I was having the same problem and I am adding what worked well for me.
onEndReachedThreshold=>onEndThreshold
<FlatList
data={this.state.data}
renderItem={({ item }) => (
<ListItem
roundAvatar
title={
<Text style={{textAlign: 'left'}}> {item.name.first} {item.name.last}</Text>
}
subtitle={
<Text style={{textAlign: 'left'}}>{item.email}</Text>
}
avatar={{ uri: item.picture.thumbnail }}
containerStyle={{ borderBottomWidth: 0 }}
/>
)}
ItemSeparatorComponent={this.renderSeparator}
ListHeaderComponent={this.renderHeader}
ListFooterComponent={this.renderFooter}
keyExtractor={item => item.email}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
onEndReached={this.handleLoadMore}
onEndThreshold={0}
/>
I hope this helps someone.
This works for me:
<FlatList
data={this.state.storesList}
renderItem={({ item, index }) => renderItem(item, index)}
keyExtractor={(item, index) => item.id.toString()}
onEndReached={this.fetchMore}
onEndReachedThreshold={0.1}
ListFooterComponent={this.renderFooter}
refreshing={this.state.refreshing}
/>
renderFooter = () => {
if (this.state.refreshing) {
return <ActivityIndicator size="large" />;
} else {
return null;
}
};
fetchMore = () => {
if (this.state.refreshing){
return null;
}
this.setState(
(prevState) => {
return { refreshing: true, pageNum: prevState.pageNum + 1 };
},
() => {
this.sendAPIRequest(null , true);
}
);
};
The reason I used the following in the fetchMore function:
if (this.state.refreshing){
return null;
}
Is because when you setState to the pageNum it calls the render() function and then the fetchMore called again. This is written to prevent it.
In addition, I set:
refreshing: false
after the sendAPIRequest is done.
Pay attention about onEndReachedThreshold in FlatList:
How far from the end (in units of visible length of the list) the
bottom edge of the list must be from the end of the content to trigger
the onEndReached callback.
Meaning in my example (0.1) means: when you reach 10% of items from the bottom, the fetchMore callback is called. In my example, I have 10 items in the list, so when the last item is visible, fetchMore is called.
I'm not sure if this is exactly what you're looking for, but the code I've left below allows you to continue scrolling through a fixed set of data props. When you reach the last index, it basically wraps around to the beginning. I've achieved this by appending a copy of the first element of the supplied data to the end of the FlatList; when the user scrolls this into view, we can safely reset the scroll offset.
import React, { Component } from 'react';
import { FlatList } from 'react-native';
export default class InfiniteFlatList extends Component {
constructor(props) {
super(props);
this.state = {
};
this._flatList = null;
}
getWrappableData = (data) => {
return [...data, data[0]];
}
render = () => (
<FlatList
{ ...this.props }
ref={ (el) => this._flatList = el }
onLayout={ ({nativeEvent}) => {
const {width, height} = nativeEvent.layout;
this.setState({
width, height
});
} }
onScroll={ ({ nativeEvent }) => {
const { x } = nativeEvent.contentOffset;
if(x === (this.props.data.length * this.state.width)) {
this._flatList.scrollToOffset({x: 0, animated: false});
}
} }
data={ this.getWrappableData(this.props.data) }
pagingEnabled={true}
/>
)
};
InfiniteFlatList.defaultProps = { };
InfiniteFlatList.propTypes = { };
This assumes you want to scroll horizontally.
It probably isn't perfect; there is likely a better technique out there which uses FlatList's onEndReached callback, however this only seemed to fire once througohout. By polling the scroll offset of the FlatList, we can fire off our own equivalent as many times as needed. If you specify a getItemLayout prop, you'll be able to use scrollToIndex({index, animated?}) instead.
Aug. 5, 2019 update
On React native 0.60, one should use scrollToOffset as:
this._flatList.scrollToOffset({offset: 0, animated: false});